#!/usr/bin/perl -w
#
# Copyright (c) 2006, 2007 Michael Schroeder, Novell Inc.
# Copyright (c) 2008 Adrian Schroeter, Novell Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
#
# The Source Server
#

BEGIN {
  my ($wd) = $0 =~ m-(.*)/- ;
  $wd ||= '.';
  chdir($wd);
  unshift @INC,  "$wd/build";
  unshift @INC,  "$wd";
}

use XML::Structured ':bytes';
use POSIX;
use Fcntl qw(:DEFAULT :flock);
use Digest::MD5 ();
use Digest::SHA ();
use Data::Dumper;
use Storable ();
use Symbol;
use JSON::XS ();

use BSConfiguration;
use BSOBS;
use BSRPC ':https';
use BSHTTP;
use BSServer;
use BSUtil;
use BSFileDB;
use BSXML;
use BSProductXML;
use BSVerify;
use BSHandoff;
use BSWatcher ':https';
use BSXPath;
use BSStdServer;
use Build;
use BSPGP;
use BSSrcrep;
use BSRevision;
use BSKiwiXML;
use BSUrlmapper;
use BSRegistryServer;

use BSXPath;
use BSXPathKeys;
use BSDB;

use BSSrcServer::Remote;
use BSSrcServer::Partition;
use BSSrcServer::Access;
use BSSrcServer::Projlink;
use BSSrcServer::Link;
use BSSrcServer::Service;
use BSSrcServer::Product;
use BSSrcServer::Multibuild;
use BSSrcServer::Srcdiff;
use BSSrcServer::Blame;
use BSSrcServer::Registry;
use BSSrcServer::Signkey;
use BSSrcServer::Notify;
use BSSrcServer::LinkinfoDB;
use BSSrcServer::ScmsyncDB;
use BSSrcServer::Modulemd;
use BSSrcServer::Scmsync;

require BSSrcServer::SQLite if $BSConfig::published_db_sqlite || $BSConfig::source_db_sqlite;

# configure modules
$BSSrcServer::Projlink::getrev       = \&getrev;
$BSSrcServer::Projlink::findpackages = \&findpackages;
$BSSrcServer::Projlink::readpackage  = \&readpackage;

# links point to service expanded files
$BSSrcServer::Link::getrev = \&getrev;
$BSSrcServer::Link::lsrev_linktarget = \&lsrev_service;

$BSSrcServer::Service::getrev = \&getrev;
$BSSrcServer::Service::readpackage  = \&readpackage;
$BSSrcServer::Service::addrev = \&addrev;
$BSSrcServer::Service::addrev_obsscmproject = \&BSSrcServer::Scmsync::sync_project;
$BSSrcServer::Service::notify = \&notify;
$BSSrcServer::Service::notify_repservers = \&notify_repservers;
$BSSrcServer::Service::handlelinks = \&BSSrcServer::Link::handlelinks;

$BSSrcServer::Scmsync::notify = \&notify;
$BSSrcServer::Scmsync::notify_repservers = \&notify_repservers;
$BSSrcServer::Scmsync::runservice = \&BSSrcServer::Service::runservice_obsscm;
$BSSrcServer::Scmsync::addrev = \&addrev;

$BSSrcServer::Blame::getrev = \&getrev;
$BSSrcServer::Blame::lsrev_expanded = \&lsrev_expanded;
$BSSrcServer::Blame::lsrev_service = \&lsrev_service;

$BSSrcServer::Product::notify_repservers = \&notify_repservers;

$Build::Rpm::unfilteredprereqs = 1 if defined $Build::Rpm::unfilteredprereqs;
$Build::Rpm::conflictdeps = 1 if defined $Build::Rpm::conflictdeps;
$Build::Kiwi::repoextras = 1 if defined $Build::Kiwi::repoextras;

$BSRevision::storelinkinfo = \&BSSrcServer::LinkinfoDB::storelinkinfo;

use strict;

my $port = 5352;	#'SR'
my $proto = 'http';
$port = $1 if $BSConfig::srcserver =~ /:(\d+)$/;
$proto = $1 if $BSConfig::srcserver =~ /^(https):/;
my $proxy;
$proxy = $BSConfig::proxy if defined($BSConfig::proxy);

BSUtil::set_fdatasync_before_rename() unless $BSConfig::disable_data_sync || $BSConfig::disable_data_sync;

my $projectsdir = "$BSConfig::bsdir/projects";
my $eventdir = "$BSConfig::bsdir/events";
my $srcrep = "$BSConfig::bsdir/sources";
my $treesdir = $BSConfig::nosharedtrees ? "$BSConfig::bsdir/trees" : $srcrep;
my $requestsdir = "$BSConfig::bsdir/requests";
my $oldrequestsdir = "$BSConfig::bsdir/requests.old";
my $rundir = $BSConfig::rundir || "$BSConfig::bsdir/run";
my $diffcache = "$BSConfig::bsdir/diffcache";
my $extrepodir = "$BSConfig::bsdir/repos";

my $reqindexdb = "$BSConfig::bsdir/db/request";
my $extrepodb = "$BSConfig::bsdir/db/published";
my $sourcedb = "$BSConfig::bsdir/db/source";

my $notificationpayloaddir = "$BSConfig::bsdir/notificationpayload";

my $srcrevlay = [qw{rev vrev srcmd5 version time user comment requestid}];
my $eventlay = [qw{number time type project package repository arch}];
my $notificationlay = [qw{number type time data []}];


my $ajaxsocket = "$rundir/bs_srcserver.ajax";
my $uploaddir = "$srcrep/:upload";

my @binsufs = @BSOBS::binsufs;
my $binsufsre = join('|', map {"\Q$_\E"} @binsufs);

my $datarepoid;
my %packagequota;


sub notify {
  my ($type, $p, $payload) = @_;
  my $time = $p->{'time'} || time();
  delete $p->{'time'};
  if ($payload) {
    print "encoding payload for $p->{'project'}/$p->{'repo'} to json\n";
    my $json_payload = JSON::XS::encode_json($payload);
    my $payloadkey = "$time.".Digest::MD5::md5_hex($json_payload);
    mkdir_p($notificationpayloaddir);
    writestr("$notificationpayloaddir/.$payloadkey", "$notificationpayloaddir/$payloadkey", $json_payload);
    $p->{'payload'} = $payloadkey;
    print "notification payload for $p->{'project'}/$p->{'repo'} stored in $payloadkey\n" if $p->{'project'} && $p->{'repo'};
  }
  my @n = map {"$_=$p->{$_}"} grep {defined($p->{$_}) && !ref($p->{$_})} sort keys %{$p || {}};
  mkdir_p($eventdir);
  BSFileDB::fdb_add_i("$eventdir/lastnotifications", $notificationlay, {'type' => $type, 'time' => $time, 'data' => \@n});
}

sub prune_notificationpayloads {
  my ($cuttime) = @_;
  my @pl = ls($notificationpayloaddir);
  for my $p (@pl) {
    next unless $p =~ /^(\d+)\./;
    unlink("$notificationpayloaddir/$p") if $1 < $cuttime;
  }
}

sub prune_lastnotifications {
  my ($cutoff) = @_;
  local *F;
  return unless BSUtil::lockopen(\*F, '<', "$eventdir/lastnotifications", 1);
  my $filter = sub { $_[0]->{'number'} >= $cutoff ? 1 : 0 };
  my @l = BSFileDB::fdb_getall("$eventdir/lastnotifications", $notificationlay, undef, $filter);
  if (@l) {
    unlink("$eventdir/.lastnotifications.$$");
    if (! -e "$eventdir/.lastnotifications.$$") {
      BSFileDB::fdb_add_multiple("$eventdir/.lastnotifications.$$", $notificationlay, @l);
      rename("$eventdir/.lastnotifications.$$", "$eventdir/lastnotifications") || die("rename $eventdir/.lastnotifications.$$ $eventdir/lastnotifications: $!\n");
      #prune_notificationpayloads($l[0]->{'time'} - 240 * 3600) if -d $notificationpayloaddir;	# keep 10 days
    }
  }
  close F;
}

sub lastnotifications {
  my ($cgi) = @_;
  my $view = $cgi->{'view'};
  die("unsupported view\n") if $view && $view ne 'json';
  if (!$cgi->{'start'}) {
    # just fetch the current event number
    my $lastev = BSFileDB::fdb_getlast("$eventdir/lastnotifications", $notificationlay);
    my $lastno = $lastev ? $lastev->{'number'} : 0;
    my $ret = {'next' => $lastno + 1, 'sync' => 'lost'};
    return (JSON::XS::encode_json($ret), 'Content-Type: application/json') if $view && $view eq 'json';
    return ($ret, $BSXML::notifications);
  }
  # check if we need to prune
  if (!$BSStdServer::isajax && !$cgi->{'noprune'}) {
    my $firstev = (BSFileDB::fdb_getall("$eventdir/lastnotifications", $notificationlay, undef, sub {-1}))[0];
    if ($firstev && $cgi->{'start'} - $firstev->{'number'} > 1000) {
      my $lastev = BSFileDB::fdb_getlast("$eventdir/lastnotifications", $notificationlay);
      prune_lastnotifications($cgi->{'start'} - 500) if $lastev && $cgi->{'start'} <= $lastev->{'number'} + 1;
    }
  }
  my $filter = sub { $cgi->{'start'} > $_[0]->{'number'} ? -2 : 1 };
  my @l = BSFileDB::fdb_getall_reverse("$eventdir/lastnotifications", $notificationlay, undef, $filter);
  if ($cgi->{'block'} && !@l) {
    # pass on to AJAX
    if (!$BSStdServer::isajax) {
      my @args;
      push @args, "view=$view" if $view;
      BSHandoff::handoff_part('lastnotifications', '/lastnotifications', undef, "start=$cgi->{'start'}", 'block=1', @args);
    }
    # wait untill we have a winner
    BSWatcher::addfilewatcher("$eventdir/lastnotifications");
    my $lastev = BSFileDB::fdb_getlast("$eventdir/lastnotifications", $notificationlay);
    return undef unless $lastev;
    return undef if $cgi->{'start'} == $lastev->{'number'} + 1;	# not there yet
    if ($cgi->{'start'} == $lastev->{'number'}) {
      @l = ($lastev);
    } elsif ($cgi->{'start'} < $lastev->{'number'}) {
      @l = BSFileDB::fdb_getall_reverse("$eventdir/lastnotifications", $notificationlay, undef, $filter);
      return undef unless @l;		# just in case
    }
  }
  my $res = {};
  @l = reverse @l;
  if (@l) {
    $res->{'next'} = $l[-1]->{'number'} + 1;
  } else {
    my $lastev = BSFileDB::fdb_getlast("$eventdir/lastnotifications", $notificationlay);
    my $no = ($lastev->{'number'} || 0);
    $no = $cgi->{'start'} - 1 if $no >= $cgi->{'start'};
    $res->{'next'} = $no + 1;
  }
  if (@l && $l[0]->{'number'} > $cgi->{'start'}) {
    $res->{'sync'} = 'lost';
    @l = ();
  }
  # don't send more than 1000 notifications to the poor api
  if (@l > 1000) {
    $res->{'limit_reached'} = 1; # tell the api that there is more
    $res->{'next'} = $l[1000]->{'number'};
    @l = splice(@l, 0, 1000);
  }
  $res->{'notification'} = \@l;
  delete $_->{'number'} for @l;		# XXX: why?
  if ($view && $view eq 'json') {
    for my $l (@l) {
      my %d;
      for (@{$l->{'data'} || []}) {
        my @s = split('=', $_, 2);
        $d{$s[0]} = $s[1];
      }
      $l->{'data'} = \%d;
    }
    return (JSON::XS::encode_json($res), 'Content-Type: application/json');
  }
  for my $l (@l) {
    for (@{$l->{'data'} || []}) {
      my @s = split('=', $_, 2);
      $_ = {'key' => $s[0]};
      $_->{'_content'} = $s[1] if defined $s[1];
    }
  }
  return ($res, $BSXML::notifications);
}

sub getnotificationpayload {
  my ($cgi, $payloadkey) = @_;
  my $file = "$notificationpayloaddir/$payloadkey";
  my $fd;
  open($fd, '<', $file) || die("404 payload does not exist!\n");
  BSWatcher::reply_file($fd, 'Content-Type: application/json');
  return undef;
}

sub deletenotificationpayload {
  my ($cgi, $payloadkey) = @_;
  unlink("$notificationpayloaddir/$payloadkey");
  return $BSStdServer::return_ok;
}

sub notify_repservers {
  my ($type, $projid, $packid, $job) = @_;

  my $ev = {'type' => $type, 'project' => $projid};
  $ev->{'package'} = $packid if defined $packid;
  addevent($ev) unless $type eq 'suspendproject';

  my @args = ("type=$type", "project=$projid");
  push @args, "package=$packid" if defined $packid;
  push @args, "job=$job" if defined $job;
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my $param = {
    'uri' => "$reposerver/event",
    'request'   => 'POST',
    'background' => 1,
  };
  eval { BSWatcher::rpc($param, undef, @args) };
  if ($@) {
    print "warning: $reposerver: $@";
    if ($type eq 'project' || $type eq 'package' || $type eq 'lowprioproject') {
      # write event to file system so that it is not lost
      notify_repservers_add_queued_event("$eventdir/undelivered", $type, $projid, $packid);
    }
  }
}

sub notify_repservers_add_queued_event {
  my ($evdir, $type, $projid, $packid) = @_;
  my $ev = {'type' => $type, 'project' => $projid};
  $ev->{'package'} = $packid if defined $packid;
  my $evname = "$type:$projid";
  $evname .= "::$packid" if defined $packid;
  $evname = "${type}:::".Digest::MD5::md5_hex($evname) if length($evname) > 200;
  mkdir_p($evdir);
  eval { writexml("$evdir/.$evname.$$", "$evdir/$evname", $ev, $BSXML::event) };
  return unless $@;
  # could be a race with the rmdir in notify_repservers_send_queued_events, so retry
  mkdir_p($evdir);
  eval { writexml("$evdir/.$evname.$$", "$evdir/$evname", $ev, $BSXML::event) };
  warn("notify_repservers_add_queued_event: $@") if $@;
}

sub notify_repservers_send_queued_events {
  my ($evdir) = @_;
  for my $evname (grep {s/^\.inprogress//} sort(ls($evdir))) {
    rename("$evdir/.inprogress.$evname", "$evdir/$evname");
  }
  for my $evname (sort(ls($evdir))) {
    next if $evname =~ /^\./;
    my $ev = readxml("$evdir/$evname", $BSXML::event, 1);
    next unless $ev;
    rename("$evdir/$evname", "$evdir/.inprogress.$evname");
    my @args = ("type=$ev->{'type'}", "project=$ev->{'project'}");
    push @args, "package=$ev->{'package'}" if defined $ev->{'package'};
    my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($ev->{'project'}) : $BSConfig::reposerver;
    my $param = {
      'uri' => "$reposerver/event",
      'request'   => 'POST',
    };
    eval { BSRPC::rpc($param, undef, @args) };
    if ($@) {
      warn("notify_repservers_queued_events: $reposerver: $@");
      rename("$evdir/.inprogress.$evname", "$evdir/$evname");
    } else {
      unlink("$evdir/.inprogress.$evname");
    }
  }
}

# this is only used from getfilelist_ajax.
# as projid is a remote project, we don't know which reposerver
# we need to notify. so send the event to all of them.
# we also do not call addevent in this specific case.
sub notify_all_repservers {
  my ($type, $projid, $packid) = @_;

  my @reposervers = BSSrcServer::Partition::allreposervers();
  my @args = ("type=$type", "project=$projid");
  push @args, "package=$packid" if defined $packid;
  for my $reposerver (@reposervers) {
    my $param = {
      'uri' => "$reposerver/event",
      'request'   => 'POST',
      'background' => 1,
    };
    eval {
      BSWatcher::rpc($param, undef, @args);
    };
    print "warning: $reposerver: $@" if $@;
  }
}

sub triggerscmsync {
  my ($cgi) = @_;

  die("scmsync tracking is not enabled\n") unless $BSConfig::source_db_sqlite;

  triggerscmsyncrun($cgi, $cgi->{'scmrepository'}, $cgi->{'scmbranch'});
  triggerscmsyncrun($cgi, $cgi->{'scmrepository'}, '') if $cgi->{'isdefaultbranch'};

  return $BSStdServer::return_ok;
}

sub triggerscmsyncrun {
  my ($cgi, $scmrepo, $scmbranch) = @_;
  for my $ref (BSSrcServer::ScmsyncDB::getscmsyncpackages($scmrepo, $scmbranch)) {
    my ($projid, $packid) = @$ref;
    my $proj = BSRevision::readproj_local($projid, 1);
    next unless $proj;
    if ($packid eq '_project') {
      die("$projid is a remote project\n") if $proj->{'remoteurl'};
      BSSrcServer::Service::runservice_obsscm($cgi, $projid, $packid, $proj->{'scmsync'}) if $proj->{'scmsync'};
    } else {
      my $pack = BSRevision::readpack_local($projid, $packid, 1);
      next unless $pack;
      my $projectscmsync;
      # the package url is used for the project scm URL. Do not use it if it is only defined on package level
      $projectscmsync = $pack->{'url'} if $proj->{'scmsync'};
      BSSrcServer::Service::runservice_obsscm($cgi, $projid, $packid, $pack->{'scmsync'}, $projectscmsync) if $pack->{'scmsync'};
    }
  }
}

sub triggerservicerun {
  my ($cgi, $projid, $packid) = @_;
  if ($packid eq '_project') {
    my $proj = BSRevision::readproj_local($projid);
    die("$projid is a remote project\n") if $proj->{'remoteurl'};
    if ($proj->{'scmsync'}) {
      BSSrcServer::Service::runservice_obsscm($cgi, $projid, $packid, $proj->{'scmsync'});
      return $BSStdServer::return_ok;
    }
    die("triggerservicerun does not work with _project\n");
  }
  my $rev = getrev($projid, $packid);
  my $pack = BSRevision::readpack_local($projid, $packid, 1);
  if ($pack && $pack->{'scmsync'}) {
    my $projectscmsync;
    # the package url is used for the project scm URL. Do not use it if it is only defined on package level
    my $proj = BSRevision::readproj_local($projid);
    $projectscmsync = $pack->{'url'} if $proj->{'scmsync'};
    BSSrcServer::Service::runservice_obsscm($cgi, $projid, $packid, $pack->{'scmsync'}, $projectscmsync);
    return $BSStdServer::return_ok;
  }
  my $linkinfo = {};
  my $files = BSRevision::lsrev($rev, $linkinfo);
  $cgi->{'triggerservicerun'} = 1;	# hack
  if ($BSConfig::old_style_services) {
    # old style, just run the service again...
    BSSrcServer::Service::runservice($cgi, $rev, $files);
  } else {
    my $servicemark = BSSrcServer::Service::genservicemark($projid, $packid, $files);
    if ($servicemark || $linkinfo->{'xservicemd5'} || $packid eq '_product') {
      # have to do a new commit...
      $cgi->{'comment'} ||= 'trigger service run';
      $cgi->{'servicemark'} = $servicemark;
      $rev = addrev_runservice($cgi, $projid, $packid, $files);
    } else {
      die("404 no source service defined!\n");
    }
  }
  return $BSStdServer::return_ok;
}

sub waitservicerun {
  my ($cgi, $projid, $packid) = @_;
  die("not implemented for old style services\n") if $BSConfig::old_style_services;
  if (!$BSStdServer::isajax) {
    my $servicemark;
    my $serror;
    my $pack = BSRevision::readpack_local($projid, $packid, 1);
    if ($pack && $pack->{'scmsync'}) {
      $servicemark = BSSrcServer::Service::genservicemark_obsscm($projid, $packid);
      $serror = BSSrcrep::getserviceerror($projid, $packid, $servicemark);
    } else {
      my $rev = getrev($projid, $packid);
      my $linkinfo = {};
      my $files = BSRevision::lsrev($rev, $linkinfo);
      $servicemark = $linkinfo->{'xservicemd5'};
      return $BSStdServer::return_ok unless $servicemark;
      eval { BSSrcServer::Service::handleservice($rev, $files, $servicemark) };
      $serror = $@;
      chomp $serror;
    }
    return $BSStdServer::return_ok unless $serror;
    die("$serror\n") if $serror !~ /service in progress/;
    # pass on to ajax
    BSHandoff::handoff("/source/$projid/$packid", undef, 'cmd=waitservice', "servicemark=$servicemark");
  }
  my $servicemark = $cgi->{'servicemark'};
  die("need a servicemark\n") unless $servicemark;
  BSWatcher::addfilewatcher(BSSrcrep::serviceerrorfile($projid, $packid, $servicemark));
  my $serror = BSSrcrep::getserviceerror($projid, $packid, $servicemark);
  return $BSStdServer::return_ok unless $serror;
  return undef if $serror =~ /service in progress/;
  die("$serror\n");
}

sub mergeservicerun {
  my ($cgi, $projid, $packid) = @_;
  my $rev = getrev($projid, $packid);
  my $linkinfo = {};
  my $files = BSRevision::lsrev($rev, $linkinfo);
  my $servicemark = $linkinfo->{'xservicemd5'};
  die("package has no service\n") unless $servicemark;
  $files = BSSrcServer::Service::handleservice($rev, $files, $servicemark);
  # merge
  for (sort keys %$files) {
    next unless /^_service:.*:(.*?)$/s;
    die("cannot create a link from a service") if $1 eq '_link';
    $files->{$1} = $files->{$_};
    delete $files->{$_};
    BSSrcrep::copyonefile($projid, $packid, $1, $projid, $packid, $_, $files->{$1});
  }

  # filter services, but the "buildtime" ones
  my $services = BSRevision::revreadxml($rev, '_service', $files->{'_service'}, $BSXML::services, 1) || {};
  my @btservices = grep {$_->{'mode'} && $_->{'mode'} eq 'buildtime'} @{$services->{'service'} || []};
  if (@btservices) {
    # write services using buildtime back
    $services->{'service'} = \@btservices;
    mkdir_p($uploaddir);
    writexml("$uploaddir/$$", undef, $services, $BSXML::services);
    $files->{'_service'} = BSSrcrep::addfile($projid, $packid, "$uploaddir/$$", '_service');
  } else {
    delete $files->{'_service'}
  }

  $rev = addrev_runservice($cgi, $projid, $packid, $files);
  delete $rev->{'project'};
  delete $rev->{'package'};
  return ($rev, $BSXML::revision);
}

#
# return version and release of commit
#
sub getcommitinfo {
  my ($projid, $packid, $srcmd5, $files) = @_;

  # get version/release from rpm spec/deb dsc/kiwi xml file
  my $version = 'unknown';
  my $release;
  if ($files->{'_link'}) {
    # can't know the version/release of a link as it is
    # a moving target
    return ('unknown', '0');
  }
  my $cfile;
  $cfile = "$projectsdir/$projid.conf" if -e "$projectsdir/$projid.conf";
  my $bconf = Build::read_config('noarch', $cfile);
  for my $type ('spec', 'dsc', 'kiwi', 'docker', 'productcompose') {
    my $rev = {'project' => $projid, 'package' => $packid, 'srcmd5' => $srcmd5};
    my $file = findfile($rev, undef, $type, $files);
    next unless defined $file;
    my $d = Build::parse_typed($bconf, BSRevision::revfilename($rev, $file, $files->{$file}), $type);
    next unless defined $d->{'version'};
    $version = $d->{'version'};
    $release = $d->{'release'} if defined $d->{'release'};
    $version = 'unknown' if $d->{'multiversion'};
    last;
  }
  if (defined($release)) {
    if ($release =~ /(\d+)\.<B_CNT>/) {
      $release = $1;
    } elsif ($release =~ /<RELEASE(\d+)>/) {
      $release = $1;
    } elsif ($release =~ /^(\d+)/) {
      $release = $1;
    } else {
      $release = '0';
    }
  }
  $release ||= '0';
  if ($bconf->{'cicntstart'} && $bconf->{'cicntstart'} =~ /(\d+)$/) {
    my $r = $release;
    $release = $bconf->{'cicntstart'};
    $release =~  s/\d+$/$r/ if $r > $1;
  }
  return ($version, $release);
}

#
# get a revision object from a revision identifier
#
sub getrev {
  my ($projid, $packid, $revid, $linked, $missingok) = @_;
  if ($packid =~ /(?<!^_product)(?<!^_patchinfo):./) {
    my $lpackid = $packid;
    $lpackid =~ s/(?<!^_product)(?<!^_patchinfo):.*//;
    my $rev = getrev($projid, $lpackid, $revid, $linked, $missingok);
    $rev->{'originpackage'} = $packid if $rev;
    return $rev;
  }
  my $rev = BSRevision::getrev_local($projid, $packid, $revid);
  return $rev if $rev;
  my $proj = BSRevision::readproj_local($projid, 1);
  if ($proj && $proj->{'link'}) {
    $rev = BSSrcServer::Projlink::getrev_projlink($projid, $proj, $packid, $revid, $linked, $missingok);
    return $rev if $rev;
  }
  if ($revid && $revid =~ /^[0-9a-f]{32}$/) {
    $rev = BSRevision::getrev_deleted_srcmd5($projid, $packid, $revid);
    return $rev if $rev;
  }
  if (!$proj || $proj->{'remoteurl'}) {
    $proj = BSSrcServer::Remote::remoteprojid($projid);
    $rev = BSSrcServer::Remote::getrev_remote($projid, $proj, $packid, $revid, $linked, $missingok) if $proj;
    return $rev if $rev;
  }
  return {'project' => $projid, 'package' => $packid, 'srcmd5' => 'pattern', 'rev' => 'pattern'} if $packid eq '_pattern';
  if ($missingok) {
    $rev = { 'project' => $projid, 'package' => $packid, 'srcmd5' => $BSSrcrep::emptysrcmd5 };
    if ($proj && $proj->{'link'}) {
      my $vrev = BSSrcServer::Projlink::getnewvrev($projid, $proj);
      $rev->{'vrev'} = $vrev if $vrev;
    }
    return $rev;
  }
  die("404 package '$packid' does not exist\n") if -e "$projectsdir/$projid.xml";
  die("404 project '$projid' does not exist\n");
}

sub addmeta_upload {
  my ($projid, $packid, $files) = @_;

  # calculate new meta sum
  my $meta = '';
  $meta .= "$files->{$_}  $_\n" for sort keys %$files;
  my $srcmd5 = Digest::MD5::md5_hex($meta);
  mkdir_p($uploaddir);
  mkdir_p("$projectsdir/$projid.pkg");
  writestr("$uploaddir/addmeta$$", "$projectsdir/$projid.pkg/$packid.upload-MD5SUMS", $meta);
  return $srcmd5;
}

##
# generate_commit_flist($files_old, $files_new)
#
#   $files_old/$files_new are hash references as returned by lsrep
#
#   returns a list of changed files categorized similar to svn commit mails
#
sub generate_commit_flist {
  my $ret = "";
  my %categorized_files;
  my ($files_old, $files_new) = @_;
  my %files_all = (%$files_new, %$files_old);
  for my $fname (sort keys %files_all) {
    if(!$files_old->{$fname}) {
      my $flist = $categorized_files{"Added:"} ||= [];
      push(@$flist, $fname);
    } elsif(!$files_new->{$fname}) {
      my $flist = $categorized_files{"Deleted:"} ||= [];
      push(@$flist, $fname);
    } elsif($files_old->{$fname} ne $files_new->{$fname}) {
      my $flist = $categorized_files{"Modified:"} ||= [];
      push(@$flist, $fname);
    }
  }

  for my $cat (sort keys %categorized_files) {
    $ret .= "$cat\n";
    for my $fname (@{$categorized_files{$cat}}) {
      $ret .= "  $fname\n";
    }
    $ret .= "\n";
  }
  return $ret;
}

#
# create a new revision from a file list, returns revision object
#
sub addrev {
  my ($cgi, $projid, $packid, $files, $target) = @_;
  die("404 project '$projid' does not exist\n") unless -e "$projectsdir/$projid.xml";
  if ($packid =~ /^_product:/) {
    die("403 package '$packid' is read-only if a '_product' package exists\n") if -e "$projectsdir/$projid.pkg/_product.xml";
  }
  die("403 package '$packid' is read-only\n") if $packid =~ /(?<!^_product)(?<!^_patchinfo):./;
  die("_project must not be a link\n") if $packid eq '_project' && $files->{'_link'};
  my $user = $cgi->{'user'};
  my $comment = $cgi->{'comment'};
  my $requestid = $cgi->{'requestid'};
  $user = '' unless defined $user;
  $user = 'unknown' if $user eq '';
  $comment = '' unless defined $comment;
  $user = str2utf8xml($user);
  $comment = str2utf8xml($comment);

  # check if the commit will need a service run
  my $servicemark;
  delete $files->{'/SERVICE'};	# just in case...
  if (!$BSConfig::old_style_services && $packid ne '_project') {
    if ($cgi->{'noservice'}) {
      ($servicemark, $files) = BSSrcServer::Service::servicemark_noservice($cgi, $projid, $packid, $files, $target, $cgi->{'lxservicemd5'});
    } else {
      # we do not want any generated files in the commit!
      delete $files->{$_} for grep {/^_service[:_]/} keys %$files;
      $servicemark = exists($cgi->{'servicemark'}) ? $cgi->{'servicemark'} : BSSrcServer::Service::genservicemark($projid, $packid, $files, $target);
    }
  }
  if ($packid eq '_pattern' && ! -e "$projectsdir/$projid.pkg/$packid.xml") {
    # upgrade pseudo _pattern package to real package
    my $pack = {
      'name' => $packid,
      'project' => $projid,
      'title' => 'pseudo package to store pattern information',
      'description' => "pseudo package to store pattern information\n",
    };
    mkdir_p($uploaddir);
    writexml("$uploaddir/$$.2", undef, $pack, $BSXML::pack);
    BSRevision::addrev_meta_replace($cgi, $projid, $packid, [ "$uploaddir/$$.2", "$projectsdir/$projid.pkg/$packid.xml", '_meta' ]);
  }
  die("404 package '$packid' does not exist\n") unless $packid eq '_project' || -e "$projectsdir/$projid.pkg/$packid.xml";
  if ($target && $target eq 'upload') {
    my $srcmd5 = addmeta_upload($projid, $packid, $files);
    my $filenames = join( ', ', keys %$files);
    notify("SRCSRV_UPLOAD", {project => $projid, package => $packid, filename => $filenames, comment => $comment, 
                             target => $target, requestid => $requestid, user => $user});
    return {'project' => $projid, 'package' => $packid, 'rev' => 'upload', 'srcmd5' => $srcmd5};
  } elsif ($target && $target eq 'repository') {
    # repository only upload.
    return {'project' => $projid, 'package' => $packid, 'rev' => 'repository', 'srcmd5' => $BSSrcrep::emptysrcmd5};
  } elsif (defined($target)) {
    # internal version only upload.
    my $srcmd5 = BSSrcrep::addmeta($projid, $packid, $files);
    return {'project' => $projid, 'package' => $packid, 'rev' => $srcmd5, 'srcmd5' => $srcmd5};
  }
  die("bad projid\n") if $projid =~ /\// || $projid =~ /^\./;
  die("bad packid\n") if $packid =~ /\// || $packid =~ /^\./;
  die("bad files (slash)\n") if grep {/\// && $_ ne '/SERVICE'} keys %$files;
  die("bad files (glyph)\n") if grep {!/^[0-9a-f]{32}$/} values %$files;

  if ($packid ne '_project') {
    my $pack = BSRevision::readpack_local($projid, $packid);
    die("Package $packid is locked\n") if $pack->{'lock'} && $pack->{'lock'}->{'enabled'};
    die("Package $packid is controlled by obs-scm\n") if $pack->{'scmsync'} && !$cgi->{'commitobsscm'};
  }

  if ($files->{'_patchinfo'}) {
    die("bad files in patchinfo container\n") if grep {$_ ne '_patchinfo' && $_ ne '_scmsync.obsinfo'} keys %$files;
    my $p = BSRevision::revreadxml({'project' => $projid, 'package' => $packid}, '_patchinfo', $files->{'_patchinfo'}, $BSXML::patchinfo);
    BSVerify::verify_patchinfo($p);
  }

  # create tree entry
  $files->{'/SERVICE'} = $servicemark if $servicemark;
  my $srcmd5 = BSSrcrep::addmeta($projid, $packid, $files);
  delete $files->{'/SERVICE'};

  my $rev = {'srcmd5' => $srcmd5, 'time' => time(), 'user' => $user, 'comment' => $comment, 'requestid' => $requestid};

  if ($packid eq '_product') {
    BSSrcServer::Product::expandproduct($projid, $packid, $rev, $files, $user, 1);
  }

  if ($packid ne '_project' && $packid ne '_pattern') {
    my ($version, $release) = getcommitinfo($projid, $packid, $srcmd5, $files);
    $rev->{'version'} = $version;
    $rev->{'vrev'} = $release;
  }
  
  my $rev_old = getrev($projid, $packid);
  $rev_old->{'keepsignkey'} = 1;
  my $files_old = BSRevision::lsrev($rev_old);
  delete $rev_old->{'keepsignkey'};
  my $filestr = generate_commit_flist($files_old, $files);

  $rev->{'version'} = $cgi->{'version'} if defined $cgi->{'version'};
  $rev->{'vrev'} = $cgi->{'vrev'} if defined $cgi->{'vrev'};
  if ($cgi->{'time'}) {
    die("specified time is less than time in last commit\n") if ($rev_old->{'time'} || 0) > $cgi->{'time'};
    $rev->{'time'} = $cgi->{'time'};
  }

  my $acceptinfo;
  if ($requestid) {
    $acceptinfo = {};
    $acceptinfo->{'osrcmd5'} = $rev_old->{'srcmd5'} if $rev_old->{'srcmd5'} ne 'empty';
    if ($files_old->{'_link'}) {
      # see if we can expand it
      eval {
	my %rev = %$rev_old;
	my $xfiles = BSSrcServer::Link::handlelinks(\%rev, $files_old);
	$acceptinfo->{'oxsrcmd5'} = $rev{'srcmd5'} if ref $xfiles;
      };
    }
  }
  if ($packid eq '_project') {
    $rev = BSRevision::addrev_local($cgi, $projid, $packid, $rev);
    if ($acceptinfo && $cgi->{'withacceptinfo'}) {
      $acceptinfo->{'rev'} = $rev->{'rev'};
      $acceptinfo->{'srcmd5'} = $rev->{'srcmd5'};
      $rev->{'acceptinfo'} = $acceptinfo;
    }
    # check if the commit included a project config change
    if (($files_old->{'_config'} || '') ne ($files->{'_config'} || '')) {
      notify_repservers('project', $projid);
      notify("SRCSRV_UPDATE_PROJECT_CONFIG", { "project" => $projid, "files" => $filestr, "comment" => $comment, "sender" => $user });
    }
    return $rev;
  }

  # help a little with link<->nolink and singleversion<->multiversion changes
  if (defined($rev->{'version'}) && defined($rev_old->{'version'}) && !defined($cgi->{'vrev'})) {
    # if this is a known -> unknown version change, max with vrev of last commit
    # same for unknown -> known
    if (($rev->{'version'} eq 'unknown' && $rev_old->{'version'} ne 'unknown') ||
        ($rev->{'version'} ne 'unknown' && $rev_old->{'version'} eq 'unknown')) {
      my $l_old = 0;
      $l_old = $1 if $rev_old->{'vrev'} =~ /(\d+)$/;
      my $l_new = 0;
      $l_new = $1 if $rev->{'vrev'} =~ /(\d+)$/;
      $rev->{'vrev'} =~ s/\d+$/$l_old + 1/e if $l_old + 1 > $l_new;
    }
  }

  # add to revision database
  $rev = BSRevision::addrev_local($cgi, $projid, $packid, $rev, $files);

  # update request acceptinfo
  if ($acceptinfo && $cgi->{'withacceptinfo'}) {
    $acceptinfo->{'rev'} = $rev->{'rev'};
    $acceptinfo->{'srcmd5'} = $rev->{'srcmd5'};
    if ($files->{'_link'}) {
      # see if we can expand it
      eval {
	my %rev = %$rev;
	my $xfiles = BSSrcServer::Link::handlelinks(\%rev, $files);
	$acceptinfo->{'xsrcmd5'} = $rev{'srcmd5'} if ref $xfiles;
      };
    }
    $rev->{'acceptinfo'} = $acceptinfo;
  }

  # send out notification
  notify("SRCSRV_COMMIT", {project => $projid, package => $packid, files => $filestr, rev => $rev->{'rev'}, user => $user, comment => $comment, 'requestid' => $requestid});
  $rev_old->{'version'} = "unknown" unless defined($rev_old->{'version'});
  if (defined($rev->{'version'}) && defined($rev_old->{'version'}) && $rev->{'version'} ne $rev_old->{'version'}) {
    notify("SRCSRV_VERSION_CHANGE", {project => $projid, package => $packid, files => $filestr, rev => $rev->{'rev'},
                                     oldversion => $rev_old->{'version'}, newversion => $rev->{'version'},
                                     user => $user, comment => $comment, 'requestid' => $requestid});
  }

  notify_repservers('package', $projid, $packid) unless $cgi->{'commitobsscm'};

  # put marker back
  $files->{'/SERVICE'} = $servicemark if $servicemark;
  return $rev;
}

sub addrev_runservice {
  my ($cgi, $projid, $packid, $files, $target) = @_;
  my $rev = addrev($cgi, $projid, $packid, $files, $target);
  BSSrcServer::Service::runservice($cgi, $rev, $files) unless $packid eq '_project' || $cgi->{'noservice'};
  return $rev;
}

# returns service expanded filelist
# modifies $rev->{'srcmd5'}
sub lsrev_service {
  my ($rev, $linkinfo) = @_;
  $linkinfo ||= {};
  my $files = BSRevision::lsrev($rev, $linkinfo);
  $files = BSSrcServer::Service::handleservice($rev, $files, $linkinfo->{'xservicemd5'}, $linkinfo) if $linkinfo->{'xservicemd5'};
  return $files;
}

# returns expanded filelist
# modifies $rev->{'srcmd5'}, $rev->{'vrev'}
sub lsrev_expanded {
  my ($rev, $linkinfo) = @_;
  my $files = lsrev_service($rev, $linkinfo);
  return $files unless $files->{'_link'};
  $files = BSSrcServer::Link::handlelinks($rev, $files, $linkinfo);
  die("$files\n") unless ref $files;
  return $files;
}

###########################################################################
###
###  project/package management
###

sub findprojects {
  my ($deleted) = @_;
  # add support for remote projects?
  return BSRevision::lsprojects_local($deleted);
}

sub findpackages {
  my ($projid, $proj, $nonfatal, $origins, $noexpand, $deleted) = @_;

  $proj ||= readxml("$projectsdir/$projid.xml", $BSXML::proj, 1);
  if (!$proj || $proj->{'remoteurl'}) {
    $proj = BSSrcServer::Remote::remoteprojid($projid);
    return BSSrcServer::Remote::findpackages_remote($projid, $proj, $nonfatal, $origins, $noexpand, $deleted) if $proj;
  }

  # get local packages
  my @packids = BSRevision::lspackages_local($projid, $deleted);
  if ($origins) {
    for (@packids) {
      $origins->{$_} = $projid unless defined $origins->{$_};
    }
  }

  # handle project links (but not if deleted)
  if ($proj && $proj->{'link'} && !$noexpand && !$deleted) {
    push @packids, BSSrcServer::Projlink::findpackages_projlink($projid, $proj, $nonfatal, $origins);
    @packids = sort(BSUtil::unify(@packids));
  }

  # add multibuild packages
  @packids = BSSrcServer::Multibuild::addmultibuildpackages($projid, $origins, @packids) unless $deleted;

  return @packids;
}

sub readpackage {
  my ($projid, $proj, $packid, $rev, $missingok) = @_;

  $proj ||= BSRevision::readproj_local($projid, 1);
  if (!$proj || $proj->{'remoteurl'}) {
    $proj = BSSrcServer::Remote::remoteprojid($projid);
    return BSSrcServer::Remote::readpackage_remote($projid, $proj, $packid, $rev, $missingok) if $proj;
  }
  my $pack = BSRevision::readpack_local($projid, $packid, 1, $rev);
  $pack->{'project'} ||= $projid if $pack;

  if (!$pack && $proj && $proj->{'link'}) {
    if (!defined($rev) || !BSRevision::readpack_local($projid, $packid, 1)) {
      $pack = BSSrcServer::Projlink::readpackage_projlink($projid, $proj, $packid, $rev, $missingok);
    }
  }

  die("404 package '$packid' does not exist in project '$projid'\n") if !$missingok && !$pack;
  return $pack;
}

sub readproject {
  my ($projid, $proj, $rev, $missingok) = @_;
  $proj ||= BSRevision::readproj_local($projid, 1, $rev);
  if (!$proj || ($proj->{'remoteurl'} && $proj->{'remoteproject'})) {
    $proj = BSSrcServer::Remote::remoteprojid($projid);
    return BSSrcServer::Remote::readproject_remote($projid, $proj, $rev, $missingok) if $proj && $proj->{'remoteproject'};
  }
  $proj->{'name'} ||= $projid if $proj;
  die("404 project '$projid' does not exist\n") if !$missingok && !$proj;
  return $proj;
}

# collect all global source services via all package and project links
sub getprojectservices {
  my ($cgi, $projid, $packid) = @_;
  my $services = BSSrcServer::Service::getprojectservices($projid, $packid);
  return ($services, $BSXML::services);
}

sub notifypackagechange {
  my ($cgi, $projid, $packid) = @_;
  notify_repservers('package', $projid, $packid);
  return $BSStdServer::return_ok;
}

# find matching .spec/.dsc/.kiwi file depending on packid and/or repoid
sub findfile {
  my ($rev, $repoid, $ext, $files) = @_;

  return undef if !$files || !$ext || $ext eq 'none';

  # map services files to their real name
  my %files = map {$_ => $_} keys %$files;
  if ($files{'_service'}) {
    for (sort keys %files) {
      next unless /^_service:.*:(.*?)$/s;
      $files{$1} = delete($files{$_}) if $files{$_};
    }
  }

  return $files{'_preinstallimage'} if $ext ne 'kiwi' && $ext ne 'productcompose' && $ext ne 'docker' && keys(%files) == 1 && $files{'_preinstallimage'};
  return $files{'Chart.yaml'} if $ext eq 'helm';
  return $files{'appimage.yml'} if $ext eq 'appimage';
  return $files{'PKGBUILD'} if $ext eq 'arch';
  return $files{'APKBUILD'} if $ext eq 'apk';
  return $files{'fissile.yml'} if $ext eq 'fissile';
  return $files{'simpleimage'} if $files{'simpleimage'};
  return $files{'snapcraft.yaml'} if $ext eq 'snapcraft';
  return (grep {/flatpak\.(?:ya?ml|json)$/} sort keys %$files)[0] if $ext eq 'flatpak';
  return (map {$files{$_}} grep {/^mkosi(?:\..*)?\.conf$/} sort keys %files)[0] if $ext eq 'mkosi';

  my $packid = $rev->{'package'};
  $packid = $1 if $rev->{'originpackage'} && $rev->{'originpackage'} =~ /:([^:]+)$/;
  if ($ext eq 'docker') {
    return $files{"Dockerfile.$packid-$repoid"} if defined($repoid) && $files{"Dockerfile.$packid-$repoid"};
    return $files{"Dockerfile.$packid"} if $files{"Dockerfile.$packid"};
    return $files{'Dockerfile'};
  }
  return $files{"$packid-$repoid.$ext"} if defined($repoid) && $files{"$packid-$repoid.$ext"};
  # 28.4.2009 mls: deleted "&& defined($repoid)"
  return $files{"$packid.$ext"} if $files{"$packid.$ext"};
  # try again without last components
  if ($packid =~ /\./) {
    my $p = $packid;
    while ($p =~ s/\.[^\.]*$//) {
      return $files{"$p.$ext"} if $files{"$p.$ext"};
    }
  }
  my @files = grep {/\.$ext$/} keys %files;
  @files = grep {/^\Q$packid\E/i} @files if @files > 1;
  return $files{$files[0]} if @files == 1;
  if (@files > 1) {
    if (!defined($repoid)) {
      # return undef;
      @files = sort @files;
      return $files{$files[0]};
    }
    @files = grep {/^\Q$packid-$repoid\E/i} @files if @files > 1;
    return $files{$files[0]} if @files == 1;
  }
  return $files{'debian.control'} if $ext eq 'dsc' && $files{'debian.control'};
  return undef;
}

#########################################################################

# set up kiwi project callback

sub kiwibootcallback {
  my ($projid, $packid) = @_;
  BSVerify::verify_projid($projid);
  BSVerify::verify_packid($packid);
  BSSrcServer::Access::checksourceaccess($projid, $packid);
  my $rev = getrev($projid, $packid);
  my $files = BSRevision::lsrev($rev);
  my $file = findfile($rev, undef, 'kiwi', $files);
  die("no kiwi file found\n") unless $file;
  my $xml = BSRevision::revreadstr($rev, $file, $files->{$file});
  return ($xml, {'project' => $projid, 'package' => $packid, 'srcmd5' => $rev->{'srcmd5'}, 'file' => $file});
}
$Build::Kiwi::bootcallback = \&kiwibootcallback;

$Build::Kiwi::urlmapper = \&BSUrlmapper::urlmapper;

#########################################################################

sub getprojquotapackage {
  my ($projid) = @_;
  if (!exists($packagequota{':packages'})) {
    my $quotaxml = readxml($BSConfig::bsquotafile, $BSXML::quota, 1);
    for my $p (@{$quotaxml->{'project'} || []}) {
      $packagequota{$p->{'name'}} = $p->{'packages'};
    }
    $packagequota{':packages'} = $quotaxml->{'packages'};
  }
  while ($projid) {
    return $packagequota{$projid} if exists $packagequota{$projid};
    last unless $projid =~ s/:[^:]*$//;
  }
  return $packagequota{':packages'};
}

sub readbuildenv {
  my ($pinfo, $rev, $files, $repoid, $arch) = @_;
  my $bifile = "_buildenv.$repoid.$arch";
  $bifile = '_buildenv' unless $files->{$bifile};
  eval {
    die("$bifile does not exist\n") unless $files->{$bifile};
    $pinfo->{'hasbuildenv'} = BSRevision::revreadstr($rev, $bifile, $files->{$bifile});
  };
  if ($@) {
    $pinfo->{'error'} = "buildenv: $@";
    chomp($pinfo->{'error'});
  }
}

sub rpm_includecallback {
  my ($rev, $files, $fn) = @_;
  my %files = map {$_ => $_} keys %$files;
  $fn =~ s/.*\///;
  if ($fn ne '_service' && $files{'_service'}) {
    for (sort keys %files) {
      next unless /^_service:.*:(.*?)$/s;
      $files{$1} = delete($files{$_}) if $files{$_};
    }
  }
  $fn = $files{$fn};
  return undef unless $fn;
  my @s = BSRevision::revstat($rev, $fn, $files->{$fn});
  return undef if !@s || $s[7] > 100000;
  return BSRevision::revreadstr($rev, $fn, $files->{$fn});
}

sub getprojpack {
  my ($cgi, $projids, $repoids, $packids, $arch) = @_;
  local *oldbsrpc = *BSRPC::rpc;
  local *BSRPC::rpc;
  die("unsupported view\n") if $cgi->{'view'} && $cgi->{'view'} ne 'storable';
  if ($cgi->{'noremote'}) {
    *BSRPC::rpc = sub {die("400 interconnect error: noremote option\n");};
  } else {
    *BSRPC::rpc = sub {
      my $r = eval { oldbsrpc(@_) };
      if ($@) {
	$@ = "interconnect error: $@" unless $@ =~ /(?:remote|interconnect) error:/;
	die($@);
      }
      return $r;
    };
  }
  $arch ||= 'noarch';
  if ($cgi->{'buildinfo'}) {
    die("getprojpack buildinfo requests must have at least one project parameter\n") unless @{$projids || []} >= 1;
    die("getprojpack buildinfo requests must have exactly one repository parameter\n") unless @{$repoids || []} == 1;
    die("getprojpack buildinfo requests must not have more than one package parameter\n") unless @{$packids || []} <= 1;
    $cgi->{'parseremote'} = 1;
    $cgi->{'withremotemap'} = 1;
    $cgi->{'withconfig'} = 1;
    $cgi->{'withrepos'} = 1;
    $cgi->{'ignoredisable'} = 1;
    $cgi->{'nopackages'} = 1 unless @{$packids || []};
  }
  if ($cgi->{'remotemaponly'}) {
    die("getprojpack remotemaponly makes no sense with packages\n") if @{$packids || []};
    $cgi->{'withremotemap'} = 1;
  }
  my $partition = $cgi->{'partition'};
  die("No such partition '$partition'\n") if $partition && $BSConfig::partitionservers && !$BSConfig::partitionservers->{$partition};
  my $remotemap = $cgi->{'withremotemap'} ? {} : undef;
  if ($remotemap && $partition) {
    $remotemap->{':partition'} = $partition;
    $remotemap->{':partitions'} = {};
  }
  $projids = [ findprojects() ] unless $projids;
  if ($partition) {
    for my $projid (splice @$projids) {
      my $par = BSSrcServer::Partition::projid2partition($projid);
      die("cannot determine partition for $projid\n") unless defined $par;
      if ($par ne $partition) {
	# check if it is remote, if not then it belongs to another partition
	my $r = BSRevision::readproj_local($projid, 1);
	$r = BSSrcServer::Remote::remoteprojid($projid) unless defined $r;
	if (!($r && $r->{'remoteurl'})) {
	  # not remote, but on wrong partition
	  # if asked for a specific project, put it in remotemap
	  next unless $remotemap && $cgi->{'project'};
	  BSSrcServer::Partition::checkpartition($remotemap, $projid, $r) if $remotemap->{':partitions'} && !$remotemap->{':partitions'}->{$projid};
	  if ($remotemap->{$projid} && $cgi->{'withconfig'} && $remotemap->{$projid}->{'partition'}) {
	    # also put config in remotemap if asked for a specific project
	    my $c = readstr("$projectsdir/$projid.conf", 1);
	    $remotemap->{$projid}->{'config'} = defined($c) ? $c : '';
	  }
	  next;
	}
      }
      push @$projids, $projid;
    }
  }

  my $limit_projids;
  if ($BSConfig::limit_projects && $BSConfig::limit_projects->{$arch}) {
    my $limit = $BSConfig::limit_projects->{$arch};
    if ($BSConfig::limit_projects_use_regex || $BSConfig::limit_projects_use_regex) {
      $limit_projids = {};
      for my $projid (@$projids) {
	$limit_projids->{$projid} = 1 if grep {$projid =~ /^$_$/} @$limit;
      }
    } else {
      $limit_projids = { map {$_ => 1} @$limit };
    }
  }

  $repoids = { map {$_ => 1} @$repoids } if $repoids;
  $packids = { map {$_ => 1} @$packids } if $packids;
  my $bconf = Build::read_config($arch);

  BSSrcServer::Projlink::enable_frozenlinks_cache();
  my %channeldata;
  my @res;
  my @projids = @$projids;
  while (@projids) {
    my $projid = shift @projids;
    my $jinfo = { 'name' => $projid };
    my $proj = BSRevision::readproj_local($projid, 1);
    if ($remotemap && (!$proj || $proj->{'remoteurl'})) {
      if ($cgi->{'project'}) {
	# fill remote data if asked for that specific project
	my $r = BSSrcServer::Remote::remoteprojid($projid);
	if ($r) {
	  eval {
	    BSSrcServer::Remote::fetchremoteproj($r, $projid, $remotemap);
	    BSSrcServer::Remote::fetchremoteconfig($r, $projid, $remotemap) if $cgi->{'withconfig'};
	  };
	}
      } elsif (!exists($remotemap->{$projid}) ) {
        # put at least the proto into the remotemap
        my $r = BSSrcServer::Remote::remoteprojid($projid);
        $remotemap->{$projid} = {%$r, 'proto' => 1} if $r;
      }
    }
    next if $cgi->{'remotemaponly'};
    if (!$proj && $cgi->{'parseremote'} && $cgi->{'project'} && $remotemap && $remotemap->{$projid}) {
      $proj = $remotemap->{$projid};
    }
    if ($cgi->{'buildinfo'}) {
      if (!@res) {
        die("project $projid does not exist\n") unless $proj;
        die("repository @{[keys %$repoids]} does not exist in $projid\n") unless grep {$repoids->{$_->{'name'}}} @{$proj->{'repository'} || []};
      } else {
	# kiwi projects, we just need the project data and config
	$packids = undef;
	$repoids = undef;;
	$cgi->{'nopackages'} = 1;
	$cgi->{'parseremote'} = 0;
      }
    }
    next unless $proj;
    for (qw{kind link scmsync}) {
      $jinfo->{$_} = $proj->{$_} if exists $proj->{$_};
    }
    if ($proj->{'scmsync'} && !$proj->{'remoteurl'}) {
      # get the scm info from the last commit
      my $lastrev = eval { BSRevision::getrev_local($projid, '_project') };
      $jinfo->{'scminfo'} = $1 if $lastrev && $lastrev->{'comment'} && $lastrev->{'comment'} =~ /\[info=([0-9a-f]{1,128})\]$/;
    }

    my $is_limited;
    $is_limited = 1 if $limit_projids && !$limit_projids->{$projid};

    my %expandedrepos;

    if ($cgi->{'withrepos'}) {
      $jinfo->{'repository'} = $proj->{'repository'} || [];
      if ($repoids && !$cgi->{'buildinfo'}) {
	$jinfo->{'repository'} = [ grep {$repoids->{$_->{'name'}}} @{$proj->{'repository'} || []} ];
      }
      if ($cgi->{'expandedrepos'}) {
	$jinfo->{'repository'} = BSUtil::clone($jinfo->{'repository'});
	for my $repo (@{$jinfo->{'repository'}}) {
	  my $base = {};
	  my @prps = expandsearchpath($projid, $repo->{'name'}, $remotemap, $base);
	  $expandedrepos{"$projid/$repo->{'name'}"} = [ @prps ];
	  for my $prp (@prps) {
	    my @s = split('/', $prp, 2);
	    $prp = {'project' => $s[0], 'repository' => $s[1]};
	  }
	  $repo->{'path'} = \@prps;
	  $repo->{'base'} = $base;
	}
      } elsif ($remotemap && !$is_limited) {
	# we always need the expanded search path with the configs so that
	# we know about the publishfilter
	for my $repo (@{$jinfo->{'repository'}}) {
	  next if $repoids && !$repoids->{$repo->{'name'}};
	  if (!$cgi->{'ignoredisable'} && defined($cgi->{'arch'})) {
	    next unless grep {$_ eq $arch} @{$repo->{'arch'} || []};
	  }
	  eval {
	    my @prps = expandsearchpath($projid, $repo->{'name'}, $remotemap);
            $expandedrepos{"$projid/$repo->{'name'}"} = [ @prps ];
	    for my $prp (@prps) {
	      my ($aprojid) = split('/', $prp, 2);
	      my $aproj = $remotemap->{$aprojid};
	      next if !$aproj || $aproj->{'error'} || defined($aproj->{'config'});
	      if ($aproj->{'partition'}) {
		$aproj->{'config'} = readstr("$projectsdir/$aprojid.conf", 1);
		$aproj->{'config'} = '' unless defined $aproj->{'config'};
	      } else {
		eval { BSSrcServer::Remote::fetchremoteconfig($aproj, $aprojid, $remotemap) };
	      }
	    }
	  };
	  $expandedrepos{"$projid/$repo->{'name'}"} = $@ if $@;
	}
      }
    }

    if ($cgi->{'buildinfo'} && !@res) {
      my $repoid = $cgi->{'repository'}->[0];
      # add all projects from the expanded path
      my $er = $expandedrepos{"$projid/$repoid"};
      die($er) unless ref($er);
      my $c = eval { concatconfigs($projid, $repoid, $remotemap, @$er) };
      for my $prp (@$er) {
        my ($p) = split('/', $prp, 2);
        next if $remotemap && $remotemap->{$p};
	push @projids, $p unless $p eq $projid || grep {$_ eq $p} @projids;
      }
      my $repo = (grep {$_->{'name'} eq $repoid} @{$jinfo->{'repository'} || []})[0];
      if ($repo && $repo->{'hostsystem'}) {
        my $conf = Build::read_config($arch, [ split("\n", $c) ]);
        $repo->{'crosshostarch'} = $conf->{'hostarch'} || $arch;
	eval {
	  my @path = expandsearchpath($projid, $repoid, $remotemap, undef, 'hostsystem');
	  $expandedrepos{"$projid/$repoid/hostsystem"} = \@path;
	  concatconfigs($projid, $repoid, $remotemap, @path);
	};
	$expandedrepos{"$projid/$repoid/hostsystem"} = $@ if $@;
      }
    }

    if ($remotemap && !$is_limited) {
      my @lprojids = map {$_->{'project'}} @{$proj->{'link'} || []};
      my %lprojids_checked;
      while (@lprojids) {
	my $lprojid = shift @lprojids;
	next if $lprojids_checked{$lprojid}++;
        my $lproj = BSSrcServer::Remote::remoteprojid($lprojid);
	eval {
	  BSSrcServer::Remote::fetchremoteproj($lproj, $lprojid, $remotemap) if $lproj;
	};
        BSSrcServer::Partition::checkpartition($remotemap, $lprojid) if $remotemap->{':partitions'} && !$remotemap->{':partitions'}->{$lprojid};
	push @lprojids, map {$_->{'project'}} @{$remotemap->{$lprojid}->{'link'} || []} if $remotemap->{$lprojid};
      }
    }

    if ($proj->{'scmsync'} && !$cgi->{'buildinfo'} && !$proj->{'remoteurl'} && !$jinfo->{'error'}) {
      my $servicemark = BSSrcServer::Service::genservicemark_obsscm($projid, '_project');
      my $serviceerror = BSSrcrep::getserviceerror($projid, '_project', $servicemark);
      if ($serviceerror && (($serviceerror !~ /service in progress/) || ! -e "$projectsdir/$projid.pkg/_project.rev")) {
	$jinfo->{'error'} = $serviceerror;
      }
    }

    if ($cgi->{'withconfig'}) {
      my $config = readstr("$projectsdir/$projid.conf", 1);
      if ($config) {
	#my $s1 = '^\s*macros:\s*$.*?^\s*:macros\s*$';
	#my $s2 = '^\s*macros:\s*$.*\Z';
	#$config =~ s/$s1//gmsi;
	#$config =~ s/$s2//gmsi;
	$jinfo->{'config'} = $config unless $config =~ /^\s*$/s;
      }
    }
    if ($cgi->{'withsrcmd5'} && -s "$projectsdir/$projid.pkg/pattern-MD5SUMS") {
      my $patterns = readstr("$projectsdir/$projid.pkg/pattern-MD5SUMS", 1);
      $jinfo->{'patternmd5'} = Digest::MD5::md5_hex($patterns) if $patterns;
    } elsif ($cgi->{'withsrcmd5'} && $cgi->{'nopackages'}) {
      # used by publisher to get patternmd5
      eval {
	my $rev = getrev($projid, '_pattern');
	my $files = lsrev_expanded($rev);
	$jinfo->{'patternmd5'} = $rev->{'srcmd5'};
      };
    }
    my @packages;
    if ($cgi->{'nopackages'} || $is_limited) {
      @packages = ();
    } elsif ($proj->{'remoteurl'}) {
      @packages = @{$cgi->{'package'}} if $cgi->{'package'} && $cgi->{'parseremote'};
    } elsif ($cgi->{'buildinfo'}) {
      @packages = @{$cgi->{'package'}} if $cgi->{'package'};
    } else {
      @packages = findpackages($projid, $proj, 2);
      if (grep {$_ eq ':missing_packages'} @packages) {
        $jinfo->{'missingpackages'} = 1;
        @packages = grep {$_ ne ':missing_packages'} @packages;
      } else {
        BSSrcServer::Multibuild::prunemultibuild($projid, \@packages);
      }
    }
    next if $repoids && !grep {$repoids->{$_->{'name'}}} @{$proj->{'repository'} || []};
    next if $packids && !grep {$packids->{$_}} @packages;
    for (qw{title description build publish debuginfo useforbuild remoteurl remoteproject download link sourceaccess privacy access lock}) {
      $jinfo->{$_} = $proj->{$_} if exists $proj->{$_};
    }
    if ($proj->{'access'}) {
      # we need the roles if the project is protected, see checkroles() in the scheduler
      my ($person, $group) = BSSrcServer::Access::mergeroles($projid, $proj);
      $jinfo->{'person'} = $person if $person && @$person;
      $jinfo->{'group'} = $group if $group && @$group;
    }
    # Check build flags in project meta data
    # packages inherit the project wide settings and may override them
    my $pdisabled;
    my $pdisable = {};
    my $penable = {};
    undef($penable) if $cgi->{'ignoredisable'};
    if ($jinfo->{'build'} && $penable) {
      for (@{$proj->{'repository'} || []}) {
        my $disen = BSUtil::enabled($_->{'name'}, $jinfo->{'build'}, 1, $arch);
        if ($disen) {
          $penable->{$_->{'name'}} = 1;
        } else {
          $pdisable->{$_->{'name'}} = 1;
        }
      }
      $pdisabled = 1 if !keys(%$penable);
    } else {
      # build is enabled
      undef($penable);
    }
    # check for a global lock
    my $plocked;
    if (!$cgi->{'ignoredisable'} && $jinfo->{'lock'}) {
      for (@{$proj->{'repository'} || []}) {
        if (BSUtil::enabled($_->{'name'}, $jinfo->{'lock'}, 0, $arch)) {
	  $plocked = 1;
	} else {
	  $plocked = undef;	# at least one repo is not locked
	  last;
	}
      }
    }

    # Check package number quota
    my $quota_exceeded;
    if ($BSConfig::bsquotafile) {
      my $pquota = getprojquotapackage($projid);
      $quota_exceeded = 1 if defined($pquota) && @packages > $pquota;
    }

    if (!$cgi->{'ignoredisable'} && !grep {!$_->{'status'} || $_->{'status'} ne 'disabled'} @{$proj->{'repository'} || []}) {
      # either no repositories or all disabled. No need to check packages
      @packages = ();
    }
    @packages = () if $cgi->{'nopackages'};
    if ($is_limited) {
      @packages = ();
      $jinfo->{'error'} = 'forbidden for this architecture';
    }
    my @pinfo;
    my %bconfs;

    my $exclude_all;
    my $exclude_repos;
    if (!$cgi->{'ignoredisable'} && defined($cgi->{'arch'})) {
      $exclude_repos = {};
      $exclude_all = 1;
      for (@{$proj->{'repository'} || []}) {
	if (grep {$_ eq $arch} @{$_->{'arch'} || []}) {
	  undef $exclude_all;
	} else {
          $exclude_repos->{$_->{'name'}} = 1;
	}
      }
    }

    # check if we have modulemd data
    my $modulemd_packid;
    my $modulemd_macros;
    if (@packages) {
      if (!$cgi->{'package'}) {
	$modulemd_packid = 'modulemd' if grep {$_ eq 'modulemd'} @packages;
      } else {
	$modulemd_packid = 'modulemd' if readpackage($projid, $proj, 'modulemd', undef, 1);
	if ($modulemd_packid) {
	  push @packages, $modulemd_packid unless grep {$_ eq $modulemd_packid} @packages;
	  $packids->{$modulemd_packid} = 1 if $packids;
	}
      }
    }

    # bring modulemd packages to the front (we need to parse them early)
    if ($modulemd_packid && @packages && $packages[0] ne $modulemd_packid) {
      @packages = grep {$_ ne $modulemd_packid} @packages;
      unshift @packages, $modulemd_packid;
    }

    my %packages_multibuild;
    for my $packid (@packages) {
      next unless $packid =~ /(?<!^_product)(?<!^_patchinfo):./ && $packid =~ /^(.*):/;
      $packages_multibuild{$1} = 1;
    }

    my @packages_delayed;
    my $packages_pass = 0;
    while (1) {
      if (!@packages) {
	last if !@packages_delayed || $packages_pass;
	$packages_pass = 1;
	BSSrcServer::Remote::fill_remote_getrev_cache();
	@packages = @packages_delayed;
	next;
      }
      my $packid = shift(@packages);

      if ($packids && !$packids->{$packid}) {
	next unless $packid =~ /(?<!^_product)(?<!^_patchinfo):./ && $packid =~ /^(.*):/;
	next unless $packids->{$1};
      }
      my $pinfo = {'name' => $packid};
      push @pinfo, $pinfo;
      my $pack;
      if ($packid =~ /(?<!^_product)(?<!^_patchinfo):./) {
        my $lpackid = $packid;
	$lpackid =~ s/:[^:]+$//;
        $pack = BSRevision::readpack_local($projid, $lpackid, 1);
      } else {
        $pack = BSRevision::readpack_local($projid, $packid, 1);
      }
      ($pack) = getpackage({}, $projid, $packid) if $proj->{'remoteurl'} && $cgi->{'parseremote'};
      $pack ||= {} if $proj->{'link'};
      if (!$pack) {
	$pinfo->{'error'} = 'package does not exist';
	next;
      }
      for (qw{build publish debuginfo useforbuild bcntsynctag scmsync sourceaccess privacy access lock releasename}) {
	$pinfo->{$_} = $pack->{$_} if $pack->{$_};
      }
      if (!$pinfo->{'build'}) {
        $pinfo->{'build'}->{'enable'} = $pack->{'enable'} if $pack->{'enable'};
        $pinfo->{'build'}->{'disable'} = $pack->{'disable'} if $pack->{'disable'};
      }
      if ($exclude_all) {
	$pinfo->{'error'} = 'excluded';
	next;
      }

      if ($plocked) {
        my $plock = $pinfo->{'lock'};
	if (!$plock || !$plock->{'disable'} || !grep {!BSUtil::enabled($_->{'name'}, $plock, 1, $arch)} @{$proj->{'repository'} || []}) {
	  $pinfo->{'error'} = 'locked';
	  next;
	}
      }

      my $enable = defined($penable) ? {%$penable} : undef;
      my $disable = {%$pdisable};
      if (!$cgi->{'ignoredisable'} && $pinfo->{'build'}) {
        for (@{$proj->{'repository'} || []}) {
          my $default = exists($disable->{$_->{'name'}}) ? 0 : 1;
          my $disen = BSUtil::enabled($_->{'name'}, $pinfo->{'build'}, $default, $arch);
          if ($disen) {
            $enable->{$_->{'name'}} = 1;
            delete $disable->{$_->{'name'}};
          } else {
            $disable->{$_->{'name'}} = 1;
            delete $enable->{$_->{'name'}};
          }
        }
      }
      undef($disable) if $enable && !keys(%$enable);
      undef($enable) if $disable && !keys(%$disable);
      if ((!$disable || $pdisabled) && $enable && !%$enable) {
	$pinfo->{'error'} = 'disabled';
	next;
      }
      if ($quota_exceeded) {
	$pinfo->{'error'} = 'quota exceeded';
	next;
      }
      if ($cgi->{'withsrcmd5'} || $cgi->{'withdeps'}) {
	my $scm_error;
	if ($pack->{'scmsync'}) {
	  my $servicemark = BSSrcServer::Service::genservicemark_obsscm($projid, $packid);
	  $scm_error = BSSrcrep::getserviceerror($projid, $packid, $servicemark);
	}
        my $rev;
	my $linked = [];
	$BSSrcServer::Remote::collect_remote_getrev = 1 unless $packages_pass;
	eval {
	  $rev = getrev($projid, $packid, 'build', $linked);
	};
	$BSSrcServer::Remote::collect_remote_getrev = 0;
        $pinfo->{'originproject'} = $rev->{'originproject'} if $rev && $rev->{'originproject'};
        $pinfo->{'linked'} = $linked if @$linked;
	if ($@) {
	  $pinfo->{'error'} = $@;
	  $pinfo->{'error'} =~ s/\n$//s;
	  if (!$packages_pass && $pinfo->{'error'} =~ /collect_remote_getrev$/) {
	    pop @pinfo;
	    push @packages_delayed, $packid;
	  }
 	  next;
	}
	if (!$rev || $rev->{'srcmd5'} eq 'empty' || $rev->{'srcmd5'} eq $BSSrcrep::emptysrcmd5) {
	  $pinfo->{'error'} = $scm_error || 'no source uploaded';
	  next;
	}
	$pinfo->{'srcmd5'} = $rev->{'srcmd5'};
	$pinfo->{'rev'} = $rev->{'rev'};
	$pinfo->{'revtime'} = $rev->{'time'} if $rev->{'time'};
	# need the relsynctag/releasename for packages from a project link
	if ($rev->{'originproject'} && !%$pack) {
	  # XXX: what about remote projects?
	  my $lpack = BSRevision::readpack_local($rev->{'originproject'}, $rev->{'package'}, 1);
	  if ($lpack) {
	    for (qw{bcntsynctag releasename}) {
	      $pinfo->{$_} = $lpack->{$_} if $lpack->{$_};
	    }
	  }
	}
	if ($scm_error) {
	  $pinfo->{'error'} = $scm_error;
	  next;
	}
	my $files;
	eval {
	  my $linkinfo = {};
          $files = BSRevision::lsrev($rev, $linkinfo);
	  if ($linkinfo->{'xservicemd5'}) {
	    $files = BSSrcServer::Service::handleservice($rev, $files, $linkinfo->{'xservicemd5'});
	    $pinfo->{'srcmd5'} = $rev->{'srcmd5'};
	  }
	  if ($linkinfo->{'xservicemd5'} || $linkinfo->{'lservicemd5'} || $linkinfo->{'lsrcmd5'}) {
	    my $meta = '';
	    $meta .= "$files->{$_}  $_\n" for sort keys %$files;
	    $pinfo->{'verifymd5'} = Digest::MD5::md5_hex($meta);
	  }
	};
	if ($@) {
	  $pinfo->{'error'} = $@;
	  $pinfo->{'error'} =~ s/\n$//s;
 	  next;
	}
	if ($files->{'_service'} && -e "$eventdir/service/${projid}::$packid") {
	  $pinfo->{'error'} = 'source update running';
 	  next;
	}
        if ($files->{'_service_error'}) {
	  $pinfo->{'error'} = 'source service failed';
 	  next;
        }
	if ($files->{'_link'}) {
	  $BSSrcServer::Remote::collect_remote_getrev = 1 unless $packages_pass;
	  eval {
	    $files = BSSrcServer::Link::handlelinks($rev, $files, {'linked' => $linked});
	  };
	  $BSSrcServer::Remote::collect_remote_getrev = 0;
	  if ($@) {
	    $files = "$@";
	    $files =~ s/\n$//;
	  }
	  if (@$linked) {
	    $pinfo->{'linked'} = $linked;
	    if ($remotemap && $remotemap->{':partitions'}) {
	      # we need to have all partition infos set for the links
	      for my $li (@$linked) {
		my $lprojid = $li->{'project'};
		next if $remotemap->{$lprojid} || $remotemap->{':partitions'}->{$lprojid};
		my $lproj = BSRevision::readproj_local($lprojid, 1);
		if ($lproj && !$lproj->{'remoteurl'}) {
		  BSSrcServer::Partition::checkpartition($remotemap, $lprojid, $lproj);
		} else {
		  $remotemap->{':partitions'}->{$lprojid} = 1;  # not on a partition
		}
	      }
	    }
	  }
	  if (!ref $files) {
	    $pinfo->{'error'} = defined($files) ? $files : "could not get file list";
	    if (!$packages_pass && $pinfo->{'error'} =~ /collect_remote_getrev$/) {
	      pop @pinfo;
	      push @packages_delayed, $packid;
	    }
	    next;
	  }
	  $pinfo->{'srcmd5'} = $rev->{'srcmd5'};
	  my $meta = '';
	  $meta .= "$files->{$_}  $_\n" for sort keys %$files;
	  $pinfo->{'verifymd5'} = Digest::MD5::md5_hex($meta);
	}
	# get rid of old multibuild packages, this assumes the main package comes first
	@packages = grep {!/^\Q$packid\E:/} @packages if $packages_multibuild{$packid};
	# get current multibuild state, ignoredisable means this is not the scheduler but
	# a buildinfo query
	my $mb = BSSrcServer::Multibuild::updatemultibuild($projid, $packid, $files, $cgi->{'ignoredisable'} ? 0 : 1);
	unshift @packages, map {"$packid:$_"} @{$mb->{'flavor'} || $mb->{'package'} || []} if $mb;

	if ($packid eq '_pattern') {
	  $jinfo->{'patternmd5'} = $pinfo->{'srcmd5'};
	  $pinfo->{'error'} = 'excluded';
	  next;
	}
	if ($files->{'_aggregate'}) {
	  my $aggregatelist = BSRevision::revreadxml($rev, '_aggregate', $files->{'_aggregate'}, $BSXML::aggregatelist, 1);
	  if (!$aggregatelist) {
	    $pinfo->{'error'} = "bad aggregatelist data";
	    next;
	  }
          eval {
	    BSVerify::verify_aggregatelist($aggregatelist);
          };
	  if ($@) {
	    my $err = $@;
	    $err =~ s/\n$//s;
	    $pinfo->{'error'} = "bad aggregatelist: $err";
	    next;
	  }
	  $pinfo->{'aggregatelist'} = $aggregatelist;
	  if ($remotemap && $aggregatelist) {
	    for my $aggregate (@{$aggregatelist->{'aggregate'} || []}) {
	      my $aprojid = $aggregate->{'project'};
	      next if $remotemap->{$aprojid} && !$remotemap->{$aprojid}->{'proto'};
	      my $aproj = BSRevision::readproj_local($aprojid, 1);
	      if (!$aproj || $aproj->{'remoteurl'}) {
		$aproj = BSSrcServer::Remote::remoteprojid($aprojid);
		eval {
		  BSSrcServer::Remote::fetchremoteproj($aproj, $aprojid, $remotemap) if $aproj;
		};
	      } else {
		BSSrcServer::Partition::checkpartition($remotemap, $aprojid, $aproj) if $remotemap->{':partitions'} && !$remotemap->{':partitions'}->{$aprojid};
	      }
	    }
	  }
	  if (($enable && %$enable) || ($disable && %$disable)) {
	    my @dinfo = ();
	    for my $repo (@{$proj->{'repository'} || []}) {
	      my $repoid = $repo->{'name'};
	      next if $repoids && !$repoids->{$repoid};
	      if ((!$disable || $disable->{$repoid}) && !(!$enable || $enable->{$repoid})) {
	        push @dinfo, {'repository' => $repoid, 'error' => 'disabled'};
		next;
	      }
	    }
	    $pinfo->{'info'} = \@dinfo if @dinfo;
	  }
	} elsif ($files->{'_patchinfo'}) {
	  my $patchinfo = BSRevision::revreadxml($rev, '_patchinfo', $files->{'_patchinfo'}, $BSXML::patchinfo, 1);
          if (!$patchinfo) {
	    $pinfo->{'error'} = "bad patchinfo data";
	    next;
	  }
          eval {
	    BSVerify::verify_patchinfo($patchinfo);
          };
	  if ($@) {
	    my $err = $@;
	    chomp $err;
	    $pinfo->{'error'} = "bad patchinfo: $err";
	    next;
	  }
	  $pinfo->{'patchinfo'} = $patchinfo;
	  if (($enable && %$enable) || ($disable && %$disable)) {
	    my @dinfo = ();
	    for my $repo (@{$proj->{'repository'} || []}) {
	      my $repoid = $repo->{'name'};
	      next if $repoids && !$repoids->{$repoid};
	      if ((!$disable || $disable->{$repoid}) && !(!$enable || $enable->{$repoid})) {
	        push @dinfo, {'repository' => $repoid, 'error' => 'disabled'};
		next;
	      }
	    }
	    $pinfo->{'info'} = \@dinfo if @dinfo;
	  }
	} elsif ($files->{'_channel'}) {
	  if (!exists($channeldata{$files->{'_channel'}})) {
	    eval {
	      my $channel = BSRevision::revreadxml($rev, '_channel', $files->{'_channel'}, $BSXML::channel);
	      BSVerify::verify_channel($channel);
	      $channeldata{$files->{'_channel'}} = $channel;
	    };
	    if ($@) {
	      my $err = $@;
	      chomp $err;
	      $channeldata{$files->{'_channel'}} = $err;
	    }
	  }
	  my $channel = $channeldata{$files->{'_channel'}} || 'bad data';
          if (!ref($channel)) {
	    $pinfo->{'error'} = "bad channel: $channel";
	    next;
	  }
          $pinfo->{'channelmd5'} = $files->{'_channel'};
	  if (($enable && %$enable) || ($disable && %$disable)) {
	    my @dinfo = ();
	    for my $repo (@{$proj->{'repository'} || []}) {
	      my $repoid = $repo->{'name'};
	      next if $repoids && !$repoids->{$repoid};
	      if ((!$disable || $disable->{$repoid}) && !(!$enable || $enable->{$repoid})) {
	        push @dinfo, {'repository' => $repoid, 'error' => 'disabled'};
		next;
	      }
	    }
	    $pinfo->{'info'} = \@dinfo if @dinfo;
	  }
	} elsif ($files->{'_modulemd.yaml'}) {
	  my $d;
	  eval {
	    my @s = BSRevision::revstat($rev, '_modulemd.yaml', $files->{'_modulemd.yaml'});
	    die("_modulemd.yaml: maximum size exceeded\n") if $s[7] > 1024*1204;
	    my $yaml = BSRevision::revreadstr($rev, '_modulemd.yaml', $files->{'_modulemd.yaml'});
	    $d = BSSrcServer::Modulemd::parse_modulemd($yaml);
	    $d->{'timestamp'} = $pinfo->{'revtime'} || $s[9];
	  };
	  if ($@) {
	    my $err = $@;
	    chomp $err;
	    $pinfo->{'error'} = "_modulemd.yaml: $err";
	    next;
	  }
	  $modulemd_macros = $d->{'macros'} if $packid eq $modulemd_packid;
	  $pinfo->{'modulemd'} = $d;
	  next;
        } elsif ($cgi->{'withdeps'}) {
	  my @dinfo;

	  if (!%$files) {
	    $pinfo->{'error'} = 'empty';
	    next;
	  }
	  $pinfo->{'constraintsmd5'} = $files->{'_constraints'} if $files->{'_constraints'};
	  if ($files->{'_buildenv'}) {
	    $pinfo->{'hasbuildenv'} = 1;
	    readbuildenv($pinfo, $rev, $files, (keys %$repoids)[0], $arch) if $cgi->{'buildinfo'};
	  }
	  for my $repo (@{$proj->{'repository'} || []}) {
	    my $repoid = $repo->{'name'};
	    next if $repoids && !$repoids->{$repoid};

	    my $rinfo = {'repository' => $repoid};
	    push @dinfo, $rinfo;
	    if ($exclude_repos && $exclude_repos->{$repoid}) {
	      $rinfo->{'error'} = 'excluded';
	      next;
	    }
	    if ((!$disable || $disable->{$repoid}) && !(!$enable || $enable->{$repoid})) {
	      $rinfo->{'error'} = 'disabled';
	      next;
	    }
            if (!$bconfs{$repoid}) {
	      print "calculating config for $projid/$repoid $arch\n";
	      my $path = $expandedrepos{"$projid/$repoid"};
	      if (!$path) {
	        eval {
		  my @path = expandsearchpath($projid, $repoid, $remotemap);
		  $expandedrepos{"$projid/$repoid"} = \@path;
		};
		$expandedrepos{"$projid/$repoid"} = $@ if $@;
		$path = $expandedrepos{"$projid/$repoid"};
	      }
	      eval {
		die($path) unless ref $path;
		my $c = concatconfigs($projid, $repoid, $remotemap, @$path);
		$c = Build::combine_configs($c, "Macros:\n$modulemd_macros") if $modulemd_macros;
	        $bconfs{$repoid} = Build::read_config($arch, [ split("\n", $c) ]);
	      };
	      if ($@) {
	        my $err = $@;
	        chomp $err;
	        $bconfs{$repoid} = {'error' => $err};
	      }
            }
	    my $conf = $bconfs{$repoid};
	    if ($conf->{'error'}) {
	      $rinfo->{'error'} = $conf->{'error'};
	      next;
	    }
	    if ($conf->{'parse_error'} && !$BSConfig::ignore_project_config_errors && $cgi->{'buildinfo'}) {
	      $rinfo->{'error'} = $conf->{'parse_error'};
	      next;
	    }
	    if ($repo->{'hostsystem'}) {
	      $repo->{'crosshostarch'} = $conf->{'hostarch'} || $arch;
	      my $path = $expandedrepos{"$projid/$repoid/hostsystem"};
	      if (!$path) {
	        eval {
		  my @path = expandsearchpath($projid, $repoid, $remotemap, undef, 'hostsystem');
		  $expandedrepos{"$projid/$repoid/hostsystem"} = \@path;
		  concatconfigs($projid, $repoid, $remotemap, @path);
		};
		$expandedrepos{"$projid/$repoid/hostsystem"} = $@ if $@;
		$path = $expandedrepos{"$projid/$repoid/hostsystem"};
	      }
	      if (!ref($path)) {
		chomp $path;
		$rinfo->{'error'} = $path;
		next;
	      }
	      if ($repo->{'crosshostarch'} eq $arch) {
		# we're doing native builds, switch to hostsystem conf
		if (!$bconfs{"$repoid/$arch"}) {
		  eval {
		    my $c = concatconfigs($projid, $repoid, $remotemap, @$path);
		    $c = Build::combine_configs($c, "Macros:\n$modulemd_macros") if $modulemd_macros;
		    $bconfs{"$repoid/$arch"} = Build::read_config($arch, [ split("\n", $c) ]);
		  };
		  if ($@) {
		    my $err = $@;
		    chomp $err;
		    $bconfs{"$repoid/$arch"} = {'error' => $err};
		  }
		}
		$conf = $bconfs{"$repoid/$arch"};
		if ($conf->{'error'}) {
		  $rinfo->{'error'} = $conf->{'error'};
		  next;
		}
		if ($conf->{'parse_error'} && !$BSConfig::ignore_project_config_errors && $cgi->{'buildinfo'}) {
		  $rinfo->{'error'} = $conf->{'parse_error'};
		  next;
		}
		if ($conf->{'hostarch'} && $conf->{'hostarch'} ne $arch) {
		  $rinfo->{'error'} = "native arch confusion: $conf->{'hostarch'}";
		  next;
		}
	      }
	    }
	    my $type = $conf->{'type'};
	    if (!$type || $type eq 'UNDEFINED') {
	      $rinfo->{'error'} = 'bad build configuration, no build type defined or detected';
	      next;
	    }
            my $file = findfile($rev, $repoid, $type, $files);
	    if (!defined($file)) {
	      $rinfo->{'error'} = 'excluded';
	      next;
	    }
	    $rinfo->{'file'} = $file;
	    my $buildtype = Build::recipe2buildtype($file);
	    if (!$buildtype) {
	      $rinfo->{'error'} = "don't know how to build $file";
	      next;
	    }
	    $rinfo->{'hasbuildenv'} = 1 if $files->{'_buildenv'} || $files->{"_buildenv.$repoid.$arch"};
	    if (($type eq 'kiwi' || $buildtype eq 'kiwi') && $BSConfig::kiwiprojects && !$cgi->{'ignoredisable'}) {
	      my %kiwiprojects = map {$_ => 1} @$BSConfig::kiwiprojects;
	      if (!$kiwiprojects{$projid}) {
		$rinfo->{'error'} = 'kiwi image building is not enabled for this project';
	        next;
	      }
	    }
	    # get build dependency info
	    local $conf->{'buildflavor'};
	    local $conf->{'obspackage'} = $packid;
	    if ($packid =~ /(?<!^_product)(?<!^_patchinfo):./) {
	      $packid =~ /^(.*):(.*?)$/;
	      $conf->{'obspackage'} = $1;
	      $conf->{'buildflavor'} = $2;
	    }
	    my $d;
	    local $Build::Rpm::includecallback = sub { rpm_includecallback($rev, $files, @_) };
	    eval {
	      $d = Build::parse_typed($conf, BSRevision::revfilename($rev, $file, $files->{$file}), $buildtype);
	    };
	    if ($@) {
	      $d = {'error' => $@};
	      $d->{'error'} =~ s/\n.*//s;
	    }
	    $d = { 'error' => "can not parse $file" } unless $d;
	    data2utf8xml($d);
	    if ($d->{'error'}) {
	      $rinfo->{'error'} = $d->{'error'};
	      next;
	    }
	    if (!defined($d->{'name'})) {
	      $rinfo->{'error'} = "can not parse name from $file";
	      next;
	    }
	    if ($repo->{'crosshostarch'}) {
	      if ($repo->{'crosshostarch'} eq $arch && !$d->{'nativebuild'}) {
	        $rinfo->{'error'} = 'excluded';
		next;
	      }
	      if ($repo->{'crosshostarch'} ne $arch && $d->{'nativebuild'}) {
	        $rinfo->{'error'} = 'excluded';
		next;
	      }
	    }
	    my $version = defined($d->{'version'}) ? $d->{'version'} : 'unknown';
	    $pinfo->{'versrel'} ||= "$version-$rev->{'vrev'}";
	    $rinfo->{'name'} = $d->{'name'};
	    $rinfo->{'dep'} = $d->{'deps'};
	    if ($d->{'prereqs'}) {
	      my %deps = map {$_ => 1} (@{$d->{'deps'} || []}, @{$d->{'subpacks'} || []});
	      my @prereqs = grep {!$deps{$_} && !/^%/} @{$d->{'prereqs'}};
	      $rinfo->{'prereq'} = \@prereqs if @prereqs;
	    }
	    $rinfo->{'onlynative'} = $d->{'onlynative'} if @{$d->{'onlynative'} || []};
	    $rinfo->{'alsonative'} = $d->{'alsonative'} if @{$d->{'alsonative'} || []};
	    $rinfo->{'nativebuild'} = 1 if $d->{'nativebuild'};
	    $rinfo->{'nouseforbuild'} = 1 if $d->{'nouseforbuild'};
	    $rinfo->{'constraint'} = $d->{'buildconstraint'} if @{$d->{'buildconstraint'} || []};
	    $rinfo->{'bcntsynctag'} = $d->{'bcntsynctag'} if $d->{'bcntsynctag'};
            # add all source services to be used at build time
	    if ($files->{'_service'}) {
              my $services = BSRevision::revreadxml($rev, '_service', $files->{'_service'}, $BSXML::services, 1) || {};
              for my $service (@{$services->{'service'} || []}) {
                next unless $service->{'mode'} && $service->{'mode'} eq 'buildtime';
                push @{$rinfo->{'buildtimeservice'}}, $service->{'name'};
              }
            }
	    # KIWI Products support debugmedium and sourcemedium filters
	    my $imagetype = $type eq 'kiwi' && $d->{'imagetype'} ? ($d->{'imagetype'}->[0] || '') : '';
	    if ($type eq 'kiwi' && $imagetype eq 'product') {
	      $rinfo->{'nodbgpkgs'} = 1 if defined($d->{'debugmedium'}) && $d->{'debugmedium'} <= 0;
	      $rinfo->{'nosrcpkgs'} = 1 if defined($d->{'sourcemedium'}) && $d->{'sourcemedium'} <= 0;
	    }
	    if ($type eq 'productcompose') {
	      $rinfo->{'nodbgpkgs'} = 1 if defined($d->{'debugmedium'}) && $d->{'debugmedium'} <= 0;
	      $rinfo->{'nosrcpkgs'} = 1 if defined($d->{'sourcemedium'}) && $d->{'sourcemedium'} <= 0;
	    }
	    if ($arch eq 'local' && defined($BSConfig::localarch)) {
	      # the local arch only builds kiwi products (and the special types above)
	      $rinfo->{'error'} = 'excluded' unless $type eq 'productcompose' || ($type eq 'kiwi' && $imagetype eq 'product');
	    }
	    if ($type eq 'productcompose' || ($type eq 'kiwi' && $imagetype eq 'product')) {
	      # Products always build on the first repo arch
	      $rinfo->{'imagearch'} = [ @{$d->{'exclarch'}} ] if $d->{'exclarch'};
	    } else {
	      my $myarch = $conf->{'target'} ? (split('-', $conf->{'target'}))[0] : $arch;
	      $rinfo->{'error'} = 'excluded' if $d->{'exclarch'} && !grep {$_ eq $myarch} @{$d->{'exclarch'}};
	      $rinfo->{'error'} = 'excluded' if $d->{'badarch'} && grep {$_ eq $myarch} @{$d->{'badarch'}};
	    }
	    for ('imagetype', 'extrasource') {
	      $rinfo->{$_} = $d->{$_} if exists $d->{$_};
	    }
	    for (@{$d->{'path'} || []}) {
	      my $r = { 'project' => $_->{'project'}, 'repository' => $_->{'repository'} };
	      $r->{'priority'} = $_->{'priority'} if defined $_->{'priority'};
	      push @{$rinfo->{'path'}}, $r;
	    }
	    for (@{$d->{'containerpath'} || []}) {
	      my $r = { 'project' => $_->{'project'}, 'repository' => $_->{'repository'} };
	      $r->{'priority'} = $_->{'priority'} if defined $_->{'priority'};
	      push @{$rinfo->{'containerpath'}}, $r;
	    }
	    if ($remotemap && ($rinfo->{'path'} || $rinfo->{'containerpath'})) {
	      my @kiwipath = (@{$rinfo->{'path'} || []}, @{$rinfo->{'containerpath'} || []});
	      @kiwipath = map {"$_->{'project'}/$_->{'repository'}"} grep {$_->{'project'} ne '_obsrepositories'} @kiwipath;
	      # simple way to fill the remote map
	      eval {
		concatconfigs($projid, $repoid, $remotemap, @kiwipath);
	      };
	      if ($cgi->{'buildinfo'} && !@res) {
		# add kiwipath projects
	        for my $prp (@kiwipath) {
		  my ($p) = split('/', $prp, 2);
		  next if $remotemap && $remotemap->{$p};
		  push @projids, $p unless $p eq $projid || grep {$_ eq $p} @projids;
	        }
	      }
	    }
	  }
	  $pinfo->{'info'} = \@dinfo if @dinfo;
	}
      }
    }
    if ($cgi->{'buildinfo'} && $modulemd_packid && $cgi->{'package'} && $cgi->{'package'}->[0] ne $modulemd_packid) {
      push @pinfo, shift @pinfo if @pinfo && $pinfo[0]->{'name'} eq $modulemd_packid;
    }
    $jinfo->{'package'} = \@pinfo;
    push @res, $jinfo;
  }
  BSSrcServer::Projlink::disable_frozenlinks_cache();

  my $ret = {'repoid' => $datarepoid, 'project' => \@res};
  if ($remotemap) {
    delete $remotemap->{':partition'};
    delete $remotemap->{':partitions'};
  }
  #print Dumper($remotemap);
  if ($remotemap && %$remotemap) {
    for my $p (sort keys %$remotemap) {
      next unless $remotemap->{$p};
      my $r = {'project' => $p};
      # keep in sync with BSXML! (we only use access/publish from the flags)
      for (qw{kind root remoteurl remoteproject remoteroot partition proto config publish access person group repository error link}) {
        $r->{$_} = $remotemap->{$p}->{$_} if defined($remotemap->{$p}->{$_});
      }
      $r->{'error'} =~ s/\n$// if $r->{'error'};
      push @{$ret->{'remotemap'}}, $r;
    }
  }
  if (%channeldata) {
    for my $md5 (sort keys %channeldata) {
      next unless ref($channeldata{$md5});
      push  @{$ret->{'channeldata'}}, {'md5' => $md5, 'channel' => $channeldata{$md5} };
    }
  }
  if ($cgi->{'view'} && $cgi->{'view'} eq 'storable') {
    return ($ret, \&BSUtil::tostorable, 'Content-Type: application/octet-stream');
  }
  return ($ret, $BSXML::projpack);
}

sub getprojectlist {
  my ($cgi) = @_;
  my @projects;
  my %projids = map {$_ => 1} @{$cgi->{'project'} || []};
  if (%projids && !$cgi->{'deleted'}) {
    for my $projid (sort keys %projids) {
      push @projects, $projid if -s "$projectsdir/$projid.xml";
    }
  } else {
    @projects = findprojects($cgi->{'deleted'});
    @projects = grep {$projids{$_}} @projects if %projids;
  }
  @projects = map {{'name' => $_}} @projects;
  return ({'entry' => \@projects}, $BSXML::dir);
}

sub getproject {
  my ($cgi, $projid) = @_;
  if ($cgi->{'withconfig'}) {
    my $config;
    die("withconfig does not support a 'rev' parameter\n") if defined $cgi->{'rev'};
    my $proj = BSRevision::readproj_local($projid, 1);
    if (!$proj || ($proj->{'remoteurl'} && $proj->{'remoteproject'})) {
      $proj = BSSrcServer::Remote::remoteprojid($projid);
      die("404 project '$projid' does not exist\n") unless $proj;
      $proj = BSSrcServer::Remote::readproject_remote($projid, $proj);
      $config = BSSrcServer::Remote::readconfig_remote($projid, $proj);
    } else {
      die("404 project '$projid' does not exist\n") unless $proj;
      $config = readstr("$projectsdir/$projid.conf", 1);
    }
    $proj->{'name'} = $projid;
    $proj->{'config'} = defined($config) ? $config : '';
    delete($proj->{'scmsync'}) if $proj->{'scmsync'} && $cgi->{'interconnect'};
    return ($proj, $BSXML::proj);
  }
  my $proj = readproject($projid, undef, $cgi->{'rev'});
  delete($proj->{'scmsync'}) if $proj->{'scmsync'} && $cgi->{'interconnect'};
  return ($proj, $BSXML::proj);
}

#########################################################################

sub updatesslcert {
  my ($projid, $pubkeyfile, $signkeyfile) = @_;
  return undef if $BSConfig::disable_sslcert_cache && $BSConfig::disable_sslcert_cache->{$projid};
  my $rev = BSRevision::getrev_meta($projid, undef);
  return undef unless $rev;
  my $files = BSRevision::lsrev($rev);
  return undef unless $files->{'_sslcert'};
  my $cert = BSSrcServer::Signkey::pubkey2sslcert($projid, $pubkeyfile, $signkeyfile);
  die("empty cert\n") unless $cert;
  mkdir_p($uploaddir);
  writestr("$uploaddir/sslcert.$$", undef, $cert);
  return "$uploaddir/sslcert.$$";
}

sub createkey {
  my ($cgi, $projid) = @_;
  $cgi->{'comment'} ||= 'create sign key';
  die("don't know how to create a key\n") unless $BSConfig::sign;
  die("404 project $projid does not exist\n") unless -s "$projectsdir/$projid.xml";
  my ($pubkey, $signkey) = BSSrcServer::Signkey::createkey($projid);
  mkdir_p("$projectsdir/$projid.pkg");
  mkdir_p($uploaddir);
  writestr("$uploaddir/pubkey.$$", undef, $pubkey);
  writestr("$uploaddir/signkey.$$", undef, $signkey);
  chmod(0600, "$uploaddir/signkey.$$");
  my $certfile = updatesslcert($projid, "$uploaddir/pubkey.$$", "$uploaddir/signkey.$$");
  BSRevision::addrev_meta_replace($cgi, $projid, undef,
	[ "$uploaddir/pubkey.$$",  "$projectsdir/$projid.pkg/_pubkey",  '_pubkey' ],
	[ "$uploaddir/signkey.$$", "$projectsdir/$projid.pkg/_signkey", '_signkey' ],
	[ $certfile, undef, '_sslcert' ]);
  return $BSStdServer::return_ok;
}

sub extendkey {
  my ($cgi, $projid) = @_;
  $cgi->{'comment'} ||= 'extend public key expiry date';
  die("don't know how to extend a key\n") unless $BSConfig::sign;
  die("project does not have a key\n") unless -s "$projectsdir/$projid.pkg/_pubkey";
  die("project does not have a signkey\n") unless -s "$projectsdir/$projid.pkg/_signkey";
  die("too far in the future\n") if $cgi->{'days'} && $cgi->{'days'} >= 100000;
  my $pubkey = BSSrcServer::Signkey::extendkey($projid, "$projectsdir/$projid.pkg/_pubkey", "$projectsdir/$projid.pkg/_signkey", $cgi->{'days'});
  mkdir_p($uploaddir);
  writestr("$uploaddir/pubkey.$$", undef, $pubkey);
  my $certfile = updatesslcert($projid, "$uploaddir/pubkey.$$", "$projectsdir/$projid.pkg/_signkey");
  BSRevision::addrev_meta_replace($cgi, $projid, undef,
	[ "$uploaddir/pubkey.$$",  "$projectsdir/$projid.pkg/_pubkey",  '_pubkey' ],
	[ $certfile, undef, '_sslcert' ]);
  return $BSStdServer::return_ok;
}

sub recreatesslcert {
  my ($cgi, $projid) = @_;
  $cgi->{'comment'} ||= 'recreate cert';
  die("project does not have a key\n") unless -s "$projectsdir/$projid.pkg/_pubkey";
  die("project does not have a signkey\n") unless -s "$projectsdir/$projid.pkg/_signkey";
  my $certfile = updatesslcert($projid, "$projectsdir/$projid.pkg/_pubkey", "$projectsdir/$projid.pkg/_signkey");
  die("could not update cert\n") unless $certfile || ($BSConfig::disable_sslcert_cache && $BSConfig::disable_sslcert_cache->{$projid});
  BSRevision::addrev_meta_replace($cgi, $projid, undef,
	[ $certfile, undef, '_sslcert' ]);
  return $BSStdServer::return_ok;
}

sub deletekey {
  my ($cgi, $projid) = @_;
  $cgi->{'comment'} ||= 'delete sign key';
  BSConfiguration::check_configuration_once();
  if ($BSConfig::forceprojectkeys) {
    my $pprojid = $projid;
    $pprojid =~ s/:[^:]*$//;
    my $sk;
    ($sk) = getsignkey({}, $pprojid) if $projid ne $pprojid;
    die("must have a key for signing in this or upper project\n") unless $sk;
  }
  BSRevision::addrev_meta_replace($cgi, $projid, undef,
	[ undef, "$projectsdir/$projid.pkg/_pubkey",  '_pubkey' ],
	[ undef, "$projectsdir/$projid.pkg/_signkey", '_signkey' ],
	[ undef, undef,                               '_sslcert' ]);
  rmdir("$projectsdir/$projid.pkg");
  return $BSStdServer::return_ok;
}

sub getpubkey {
  my ($cgi, $projid) = @_;
  my $pubkey;

  my $proj = BSRevision::readproj_local($projid, 1, $cgi->{'rev'});
  $proj = BSSrcServer::Remote::remoteprojid($projid) if !$proj || $proj->{'remoteurl'};
  die("404 project '$projid' does not exist\n") unless $proj;

  if ($proj->{'remoteurl'}) {
    $pubkey = BSRPC::rpc({'uri' => "$proj->{'remoteurl'}/source/$proj->{'remoteproject'}/_pubkey", 'proxy' => $proxy}, undef);
  } else {
    if ($cgi->{'rev'}) {
      my $rev = BSRevision::getrev_meta($projid, undef, $cgi->{'rev'});
      my $files = $rev ? BSRevision::lsrev($rev) : {};
      $pubkey = BSRevision::revreadstr($rev, '_pubkey', $files->{'_pubkey'}, 1) if $files->{'_pubkey'};
    } else {
      $pubkey = readstr("$projectsdir/$projid.pkg/_pubkey", 1);
    }
  }
  die("404 $projid: no pubkey available\n") unless $pubkey;
  return ($pubkey, 'Content-Type: text/plain');
}

#########################################################################

sub putproject {
  my ($cgi, $projid) = @_;
  mkdir_p($uploaddir);
  my $uploaded = BSServer::read_file("$uploaddir/$$");
  die("upload failed\n") unless $uploaded;
  my $proj = readxml("$uploaddir/$$", $BSXML::proj);
  $proj->{'name'} = $projid unless defined $proj->{'name'};
  BSVerify::verify_proj($proj, $projid);
  writexml("$uploaddir/$$.2", undef, $proj, $BSXML::proj);
  unlink("$uploaddir/$$");
  my $oldproj = BSRevision::readproj_local($projid, 1);
  notify($oldproj ? "SRCSRV_UPDATE_PROJECT" : "SRCSRV_CREATE_PROJECT", { "project" => $projid, "sender" => ($cgi->{'user'} || "unknown") });
  mkdir_p($projectsdir) || die("creating $projectsdir: $!\n");
  BSRevision::addrev_meta_replace($cgi, $projid, undef, [ "$uploaddir/$$.2", "$projectsdir/$projid.xml", '_meta' ]);
  BSConfiguration::check_configuration_once();
  if ($BSConfig::forceprojectkeys) {
    my ($sk) = getsignkey({}, $projid);
    createkey({ %$cgi, 'comment' => 'autocreate key' }, $projid) if $sk eq '';
  }

  my %except = map {$_ => 1} qw{title description person group url attributes};
  if (!BSUtil::identical($oldproj, $proj, \%except)) {
    my $type = ($cgi->{'lowprio'}) ? 'lowprioproject' : 'project';
    if ($proj->{'remoteurl'} || ($oldproj || {})->{'remoteurl'}) {
      # inform all repserves about a remote project
      # need to add the event here since notify_all_repservers() doesn't do it
      my $ev = {'type' => $type, 'project' => $projid};
      addevent($ev);
      notify_all_repservers($type, $projid);
    } else {
      notify_repservers($type, $projid);
    }
  }

  if ($proj->{'scmsync'}) {
    if ($proj->{'scmsync'} ne (($oldproj || {})->{'scmsync'} || '')) {
      # scmsync changed, trigger a service run
      print "scmsync changed: trigger $projid\n";
      BSSrcServer::ScmsyncDB::storescmsync($projid, '_project', $proj->{'scmsync'});
      eval { BSSrcServer::Service::runservice_obsscm($cgi, $projid, '_project', $proj ->{'scmsync'}) };
      warn($@) if $@;
    }
  } elsif (($oldproj || {})->{'scmsync'}) {
    BSSrcServer::ScmsyncDB::deletescmsync($projid, '_project');
  }

  $proj = BSRevision::readproj_local($projid);
  return ($proj, $BSXML::proj);
}

sub delproject {
  my ($cgi, $projid) = @_;

  $cgi->{'comment'} ||= 'project was deleted';
  die("404 project '$projid' does not exist\n") unless -e "$projectsdir/$projid.xml";

  # add delete commit to both source and meta
  BSRevision::addrev_local_replace($cgi, $projid, undef);
  BSRevision::addrev_meta_replace($cgi, $projid, undef);

  unlink("$projectsdir/$projid.conf");
  unlink("$projectsdir/$projid.xml");
  BSSrcServer::ScmsyncDB::deletescmsync($projid, '_project');
  notify_repservers('project', $projid);

  notify("SRCSRV_DELETE_PROJECT", { "project" => $projid, "comment" => $cgi->{'comment'}, "sender" => ($cgi->{'user'} || "unknown"), "requestid" => $cgi->{'requestid'} });

  # send reply
  BSServer::reply($BSStdServer::return_ok);
  BSServer::done(1);

  # now delete all packages
  eval {
    if (-d "$projectsdir/$projid.pkg") {
      mkdir_p("$projectsdir/_deleted/$projid.pkg");
      # make room in old delete area by deleting all old packages
      BSRevision::delete_deleted($cgi, $projid);
      # make sure we have mrev files for all packages
      for my $f (ls("$projectsdir/$projid.pkg")) {
	next unless $f =~ /^(.*)\.xml$/;
        BSRevision::addrev_meta_replace($cgi, $projid, $1) unless -f "$projectsdir/$projid.pkg/$1.mrev";
      }
      # now do the real delete on all of those files
      for my $f (ls("$projectsdir/$projid.pkg")) {
        if ($f =~ /^(.*)\.m?rev(?:\.del)?$/) {
	  BSRevision::delete_rev($cgi, $projid, $1, "$projectsdir/$projid.pkg/$f", "$projectsdir/_deleted/$projid.pkg/$f");
        }
        unlink("$projectsdir/$projid.pkg/$f");
      }
      rmdir("$projectsdir/$projid.pkg") || die("rmdir $projectsdir/$projid.pkg: $!\n");
    }
  };
  warn($@) if $@;
  return undef;
}

sub undeleteproject {
  my ($cgi, $projid) = @_;

  die("404 project '$projid' already exists\n") if -e "$projectsdir/$projid.xml";
  die("404 project '$projid' is not deleted\n") unless -e "$projectsdir/_deleted/$projid.pkg";
  $cgi->{'comment'} ||= 'project was undeleted';
  mkdir_p($uploaddir);
  mkdir_p("$projectsdir/$projid.pkg");
  for my $f (ls("$projectsdir/_deleted/$projid.pkg")) {
    if ($f =~ /\.m?rev\.del$/) {
      BSUtil::cp("$projectsdir/_deleted/$projid.pkg/$f", "$uploaddir/$$.2", "$projectsdir/$projid.pkg/$f");
    } elsif ($f =~ /^(.*)\.(m?rev)$/) {
      BSRevision::undelete_rev($cgi, $projid, $1, "$projectsdir/_deleted/$projid.pkg/$f", "$projectsdir/$projid.pkg/$f");
    }
  }
  notify_repservers('project', $projid);
  notify("SRCSRV_UNDELETE_PROJECT", { "project" => $projid, "comment" => $cgi->{'comment'}, "sender" => ($cgi->{'user'} || "unknown") });

  return $BSStdServer::return_ok;
}

#########################################################################

sub getpackagelist {
  my ($cgi, $projid, $repoid, $arch) = @_;
  my $origins = $cgi->{'noorigins'} ? undef : {};
  my $proj;
  $proj = checkprojrepoarch($projid, $repoid, $arch, 1) unless $cgi->{'deleted'};
  my @packages = findpackages($projid, $proj, 0, $origins, !$cgi->{'expand'}, $cgi->{'deleted'});
  for (@packages) {
    $_ = {'name' => $_};
    $_->{'originproject'} = $origins->{$_->{'name'}} if $origins && $origins->{$_->{'name'}} ne $projid;
    $_->{'originpackage'} = $1 if $_->{'name'} =~ /^(.*?)(?<!^_product)(?<!^_patchinfo):./;
  }
  return ({'entry' => \@packages}, $BSXML::dir);
}

sub getpackage {
  my ($cgi, $projid, $packid) = @_;
  if ($cgi->{'meta'} || $cgi->{'view'}) {
    $cgi->{'meta'} ||= 1;
    return getfile($cgi, $projid, $packid, '_meta');
  }
  my $proj;
  $proj = checkprojrepoarch($projid, undef, undef, 1) unless $cgi->{'deleted'};
  if ((!$proj || !$proj->{'remoteurl'}) && ($cgi->{'rev'} || $cgi->{'deleted'} || $packid eq '_project')) {
    # return the exact file here
    # we also do not look at project links
    # we return the data as string so that the md5 sum matches
    my $rev = BSRevision::getrev_meta($projid, $packid, $cgi->{'rev'}, $cgi->{'deleted'});
    my $files = BSRevision::lsrev($rev);
    die("404 _meta: no such file\n") unless $files->{'_meta'};
    my $meta = BSRevision::revreadstr($rev, '_meta', $files->{'_meta'});
    if ($cgi->{'interconnect'}) {
      my $xml = fromxml("$uploaddir/$$", $BSXML::pack);
      if ($xml->{'scmsync'}) {
        delete($xml->{'scmsync'});
        $meta = toxml($xml, $BSXML::pack);
      }
    }
    return ($meta);
  }
  my $pack = readpackage($projid, $proj, $packid, $cgi->{'rev'});
  delete($pack->{'scmsync'}) if $pack->{'scmsync'} && $cgi->{'interconnect'};
  return ($pack, $BSXML::pack);
}

sub putpackage {
  my ($cgi, $projid, $packid) = @_;
  mkdir_p($uploaddir);
  my $uploaded = BSServer::read_file("$uploaddir/$$");
  die("upload failed\n") unless $uploaded;
  my $pack = readxml("$uploaddir/$$", $BSXML::pack);
  $pack->{'name'} = $packid unless defined $pack->{'name'};
  BSVerify::verify_pack($pack, $packid);
  writexml("$uploaddir/$$.2", undef, $pack, $BSXML::pack);
  unlink("$uploaddir/$$");
  my $proj = BSRevision::readproj_local($projid);
  die("$projid is a remote project\n") if $proj->{'remoteurl'};
  die("$projid is controlled by obs-scm\n") if $proj->{'scmsync'};
  if ($packid eq '_product' && ! -e "$projectsdir/$projid.pkg/$packid.xml") {
    # creating a _product package, make sure that there is no _product:xxx package
    my @pkgs = BSRevision::lspackages_local($projid);
    die("cannot create '$packid' if _product:* packages already exist\n") if grep {/^_product:/} @pkgs;
  }
  if (($packid =~ /^_product:/) && ! -e "$projectsdir/$projid.pkg/$packid.xml") {
    die("403 cannot create '$packid' if a '_product' package exists\n") if -e "$projectsdir/$projid.pkg/_product.xml";
  }
  mkdir_p("$projectsdir/$projid.pkg");

  my $oldpack = BSRevision::readpack_local($projid, $packid, 1);
  notify($oldpack ? "SRCSRV_UPDATE_PACKAGE" : "SRCSRV_CREATE_PACKAGE", { "project" => $projid, "package" => $packid, "sender" => ($cgi->{'user'} || "unknown")});

  BSRevision::addrev_meta_replace($cgi, $projid, $packid, [ "$uploaddir/$$.2", "$projectsdir/$projid.pkg/$packid.xml", '_meta' ]);
  my %except = map {$_ => 1} qw{title description devel person group url};
  if (!BSUtil::identical($oldpack, $pack, \%except)) {
    notify_repservers('package', $projid, $packid);
  }
  if ($pack->{'scmsync'}) {
    if ($pack->{'scmsync'} ne (($oldpack || {})->{'scmsync'} || '')) {
      # scmsync changed, trigger a service run
      BSSrcServer::ScmsyncDB::storescmsync($projid, $packid, $pack->{'scmsync'});
      print "scmsync changed: trigger $projid/$packid\n";
      eval { BSSrcServer::Service::runservice_obsscm($cgi, $projid, $packid, $pack->{'scmsync'}) };
      warn($@) if $@;
    }
  } elsif (($oldpack || {})->{'scmsync'}) {
    BSSrcServer::ScmsyncDB::deletescmsync($projid, $packid);
  }
  $pack = BSRevision::readpack_local($projid, $packid);
  return ($pack, $BSXML::pack);
}

sub delpackage {
  my ($cgi, $projid, $packid) = @_;
  $cgi->{'comment'} ||= 'package was deleted';
  my $proj = BSRevision::readproj_local($projid);
  die("$projid is controlled by obs-scm\n") if $proj->{'scmsync'};
  die("404 package '$packid' does not exist in project '$projid'\n") unless -e "$projectsdir/$projid.pkg/$packid.xml";
  if ($packid =~ /^_product:/) {
    die("403 cannot delete '$packid' if a '_product' package exists\n") if -e "$projectsdir/$projid.pkg/_product.xml";
  }
  # kill upload revision
  unlink("$projectsdir/$projid.pkg/$packid.upload-MD5SUMS");

  # add delete commit to both source and meta
  BSRevision::addrev_local_replace($cgi, $projid, $packid);
  BSRevision::addrev_meta_replace($cgi, $projid, $packid);

  # now do the real delete of the package
  BSRevision::delete_rev($cgi, $projid, $packid, "$projectsdir/$projid.pkg/$packid.rev", "$projectsdir/$projid.pkg/$packid.rev.del");
  BSRevision::delete_rev($cgi, $projid, $packid, "$projectsdir/$projid.pkg/$packid.mrev", "$projectsdir/$projid.pkg/$packid.mrev.del");

  BSSrcServer::ScmsyncDB::deletescmsync($projid, $packid);

  # get rid of the generated product packages as well
  if ($packid eq '_product') {
    BSSrcServer::Product::expandproduct($projid, $packid, undef, undef);
  }
  notify_repservers('package', $projid, $packid);
  notify("SRCSRV_DELETE_PACKAGE", { "project" => $projid, "package" => $packid, "sender" => ($cgi->{'user'} || "unknown"), "comment" => $cgi->{'comment'}, "requestid" => $cgi->{'requestid'} });
  return $BSStdServer::return_ok;
}

sub undeletepackage {
  my ($cgi, $projid, $packid) = @_;
  $cgi->{'comment'} ||= 'package was undeleted';
  my $proj = BSRevision::readproj_local($projid);
  die("$projid is controlled by obs-scm\n") if $proj->{'scmsync'};
  die("403 package '$packid' already exists\n") if -e "$projectsdir/$projid.pkg/$packid.xml";
  die("403 package '$packid' was not deleted\n") unless -e "$projectsdir/$projid.pkg/$packid.rev.del";
  BSRevision::undelete_rev($cgi, $projid, $packid, "$projectsdir/$projid.pkg/$packid.mrev.del", "$projectsdir/$projid.pkg/$packid.mrev");
  if (-s "$projectsdir/$projid.pkg/$packid.rev.del") {
    BSRevision::undelete_rev($cgi, $projid, $packid, "$projectsdir/$projid.pkg/$packid.rev.del", "$projectsdir/$projid.pkg/$packid.rev");
  }
  if ($packid eq '_product') {
    my $rev = BSRevision::getrev_local($projid, $packid);
    if ($rev) {
      my $files = BSRevision::lsrev($rev);
      BSSrcServer::Product::expandproduct($projid, $packid, $rev, $files, $rev->{'user'});
    }
  }
  notify_repservers('package', $projid, $packid);
  notify("SRCSRV_UNDELETE_PACKAGE", { "project" => $projid, "package" => $packid, "sender" => ($cgi->{'user'} || "unknown"), "comment" => $cgi->{'comment'} });
  return $BSStdServer::return_ok;
}

sub lockpackage {
  my ($cgi, $projid, $packid) = @_;
  die("403 cannot lock _product package\n") if $packid eq '_product';
  $cgi->{'comment'} ||= 'package was locked';
  my $proj = BSRevision::readproj_local($projid);
  my $pack = BSRevision::readpack_local($projid, $packid);
  die("403 package '$packid' is already locked\n") if $pack->{'lock'};
  $pack->{'lock'} = { 'enable' => [ {} ] };
  mkdir_p($uploaddir);
  writexml("$uploaddir/$$", undef, $pack, $BSXML::pack);
  BSRevision::addrev_meta_replace($cgi, $projid, $packid, [ "$uploaddir/$$", "$projectsdir/$projid.pkg/$packid.xml", '_meta' ]);
  notify_repservers('package', $projid, $packid);
  return $BSStdServer::return_ok;
}

sub unlockpackage {
  my ($cgi, $projid, $packid) = @_;
  die("403 cannot unlock _product package\n") if $packid eq '_product';
  $cgi->{'comment'} ||= 'package was unlocked';
  my $proj = BSRevision::readproj_local($projid);
  my $pack = BSRevision::readpack_local($projid, $packid);
  die("403 package '$packid' is not locked\n") unless $pack->{'lock'} && $pack->{'lock'}->{'enable'};
  die("403 package '$packid' is locked in a weird way\n") if $pack->{'lock'}->{'disable'} || @{$pack->{'lock'}->{'enable'}} != 1 || %{$pack->{'lock'}->{'enable'}->[0]};
  delete $pack->{'lock'};
  mkdir_p($uploaddir);
  writexml("$uploaddir/$$", undef, $pack, $BSXML::pack);
  BSRevision::addrev_meta_replace($cgi, $projid, $packid, [ "$uploaddir/$$", "$projectsdir/$projid.pkg/$packid.xml", '_meta' ]);
  notify_repservers('package', $projid, $packid);
  return $BSStdServer::return_ok;
}

sub getpackagehistory {
  my ($cgi, $projid, $packid) = @_;
  my @res;
  my $revfile;
  $packid = '_project' unless defined $packid;

  if (!$cgi->{'deleted'}) {
    my $proj = checkprojrepoarch($projid, undef, undef, 1);
    if ($proj->{'remoteurl'}) {
      my @args = BSRPC::args($cgi, 'rev', 'meta', 'limit');
      my $h = BSRPC::rpc({'uri' => "$proj->{'remoteurl'}/source/$proj->{'remoteproject'}/$packid/_history", 'proxy' => $proxy}, $BSXML::revisionlist, @args);
      return ($h, $BSXML::revisionlist);
    }
    if ($packid ne '_project' && ! -e "$projectsdir/$projid.pkg/$packid.xml") {
      # check project links
      die("404 package '$packid' does not exist\n") unless $proj->{'link'};
      $cgi->{'_checked'} ||= {};
      $cgi->{'_checked'}->{$projid} = 1;
      for my $lprojid (map {$_->{'project'}} @{$proj->{'link'}}) {
	next if $cgi->{'_checked'}->{$lprojid};
	$cgi->{'_checked'}->{$lprojid} = 1;
	my $h;
	eval {
	  $h = (getpackagehistory($cgi, $lprojid, $packid))[0];
	};
	die($@) if $@ && $@ !~ /^404/;
	return ($h, $BSXML::revisionlist) if $h;
      }
      die("404 package '$packid' does not exist\n");
    }
  }

  $revfile = "$projectsdir/$projid.pkg";
  $revfile = "$projectsdir/_deleted/$projid.pkg" if $packid eq '_project' && $cgi->{'deleted'};
  $revfile .= $cgi->{'meta'} ? "/$packid.mrev" : "/$packid.rev";
  if ($packid ne '_project' && $cgi->{'deleted'}) {
    $revfile .= '.del';
    if (! -e $revfile && ! -e "$projectsdir/$projid.xml" && -e "$projectsdir/_deleted/$projid.pkg") {
      $revfile = "$projectsdir/_deleted/$projid.pkg/$packid.mrev";
    }
  }
  my $filter;
  if ($cgi->{'rev'} && $cgi->{'rev'} =~ /^[0-9a-f]{32}/) {
    # get all revisions with this srcmd5
    $filter = sub { return $cgi->{'rev'} eq $_[0]->{'srcmd5'} ? 1 : 0 };
  } elsif ($cgi->{'rev'}) {
    # get a specific revision
    $filter = sub { return $cgi->{'rev'} eq $_[0]->{'rev'} ? 1 : 0 };
    $cgi->{'limit'} = 1 unless defined $cgi->{'limit'};
  }
  my $startbefore = $cgi->{'startbefore'};
  if (defined $startbefore) {
    die("can't use startbefore with another filter\n") if $filter;
    my $started;
    $filter = sub { $started = 1 if $_[0]->{'rev'} < $startbefore ; $started };
  }
  for (BSFileDB::fdb_getall_reverse($revfile, $srcrevlay, $cgi->{'limit'}, $filter)) {
    $_->{'comment'} = str2utf8xml($_->{'comment'}) if $_->{'comment'};
    unshift @res, $_;
  }
  return ({'revision' => \@res}, $BSXML::revisionlist);
}

##########################################################################

sub expandsearchpath {
  my ($projid, $repoid, $remotemap, $base, $pathelement) = @_;
  my %done;
  my @ret;
  my @path = {project => $projid, repository => $repoid};
  $pathelement ||= 'path';
  while (@path) {
    my $t = shift @path;
    my $prp = "$t->{'project'}/$t->{'repository'}";
    push @ret, $prp unless $done{$prp};
    $done{$prp} = 1;
    if (!@path) {
      ($base->{'project'}, $base->{'repository'}) = ($t->{'project'}, $t->{'repository'}) if $base;
      last if $done{"/$prp"};
      my ($pid, $tid) = ($t->{'project'}, $t->{'repository'});
      my $proj = BSRevision::readproj_local($pid, 1);
      if (!$proj || $proj->{'remoteurl'}) {
	undef $proj;
	$proj = $remotemap->{$pid} if $remotemap && $remotemap->{$pid};
	if (!$proj || $proj->{'proto'}) {
          $proj = BSSrcServer::Remote::remoteprojid($pid);
          die("404 project '$pid' does not exist\n") unless $proj && $proj->{'remoteurl'};
          $proj = BSSrcServer::Remote::fetchremoteproj($proj, $pid, $remotemap);
          return () if $BSStdServer::isajax && !defined($proj);	# in progress
          die("404 project '$pid' does not exist\n") unless $proj;
        }
      }
      BSSrcServer::Partition::checkpartition($remotemap, $pid, $proj) if $remotemap && $remotemap->{':partitions'} && !$remotemap->{':partitions'}->{$pid};
      $done{"/$prp"} = 1;       # mark expanded
      my $repo = (grep {$_->{'name'} eq $tid} @{$proj->{'repository'} || []})[0];
      push @path, @{$repo->{$pathelement} || $repo->{'path'} || []} if $repo;
    } elsif ($remotemap) {
      my $pid = $t->{'project'};
      my $proj = BSRevision::readproj_local($pid, 1);
      if ((!$proj || $proj->{'remoteurl'}) && !$remotemap->{$pid}) {
        my $r = BSSrcServer::Remote::remoteprojid($pid);
        $remotemap->{$pid} = {%$r, 'proto' => 1} if $r;
      }
      BSSrcServer::Partition::checkpartition($remotemap, $pid, $proj) if $remotemap && $remotemap->{':partitions'} && !$remotemap->{':partitions'}->{$pid};
    }
  }
  return @ret;
}

sub concatconfigs {
  my ($projid, $repoid, $remotemap, @path) = @_;

  my $config = "%define _project $projid\n";
  my $macros = '';

  #$macros .= "%vendor Open Build Service\n";

  # find the sign project, this is what we use as vendor
  my $vprojid = $projid;
  while ($vprojid ne '') {
    last if -s "$projectsdir/$vprojid.pkg/_signkey";
    $vprojid =~ s/[^:]*$//;
    $vprojid =~ s/:$//;
  }
  $vprojid = $projid if $vprojid eq '';
  my $obsname = $BSConfig::obsname || 'build.opensuse.org';
  $macros .= "%vendor obs://$obsname/$vprojid\n";
  $macros .= "%_download_url $BSConfig::repodownload\n" if $BSConfig::repodownload;

  $macros .= "%_project $projid\n";
  my $lastr = '';

  my $distinfo = $repoid eq 'standard' ? $projid : "$projid / $repoid";

  my ($old_is_this_project, $old_is_in_project) = (-1, -1);
  my ($config_init, $macros_init) = ($config, $macros);
  for my $prp (reverse @path) {
    $macros .= "\n%distribution $distinfo\n%_project $projid\n" if $prp eq "$projid/$repoid";
    my ($p, $r) = split('/', $prp, 2);
    my $c;
    if (-s "$projectsdir/$p.conf") {
      $c = readstr("$projectsdir/$p.conf");
    } elsif (!-e "$projectsdir/$p.xml") {
      my $proj = BSSrcServer::Remote::remoteprojid($p);
      if ($proj && $proj->{'remoteurl'}) {
	$c = BSSrcServer::Remote::fetchremoteconfig($proj, $p, $remotemap);
	return undef if $BSStdServer::isajax && !defined($c);
      }
    }
    if ($remotemap && $remotemap->{':partitions'}) {
      BSSrcServer::Partition::checkpartition($remotemap, $p) if !$remotemap->{':partitions'}->{$p};
      $remotemap->{$p}->{'config'} = defined($c) ? $c : '' if ($remotemap->{$p} || {})->{'partition'};
    }
    next unless defined $c;

    if ("\n$c" =~ /^(.*?)\nFromScratch:/si && $1 !~ /\n[ \t]*[^\s#]/) {
      ($config, $macros) = ($config_init, $macros_init);
      ($old_is_this_project, $old_is_in_project) = (-1, -1);
      $macros .= "\n%distribution $distinfo\n%_project $projid\n" if $prp eq "$projid/$repoid";
    }

    $config .= "\n### from $p\n";
    $config .= "%define _repository $r\n";
    my $new_is_this_project = $p eq $projid ? 1 : 0;
    my $new_is_in_project = $new_is_this_project || substr($projid, 0, length($p) + 1) eq "$p:" ? 1 : 0;
    $config .= "%define _is_this_project $new_is_this_project\n" if $new_is_this_project ne $old_is_this_project;
    $config .= "%define _is_in_project $new_is_in_project\n" if $new_is_in_project ne $old_is_in_project;
    ($old_is_this_project, $old_is_in_project) = ($new_is_this_project, $new_is_in_project);
    $config .= "#!!line $p:0\n";

    if ($c =~ /^\s*:macros\s*$/im) {
      # probably some multiple macro sections with %if statements
      # flush out macros
      $macros .= "\n### from $p\n";
      $macros .= "\n%_repository $r\n";
      $config .= "\nMacros:\n$macros:Macros\n\n";
      $macros = '';
      $lastr = $r;
      my $s1 = '\A(.*^\s*:macros\s*$)(.*?)\z';	# should always match
      if ($c =~ /$s1/msi) {
        $config .= $1;
	$c = $2;
      } else {
        $config .= $c;
	$c = '';
      }
    }
    if ($c =~ /^(.*\n)?\s*macros:[^\n]*\n(.*)/si) {
      # has single macro section at end. cumulate
      $c = defined($1) ? $1 : '';
      $macros .= "\n### from $p\n";
      $macros .= "%_repository $r\n";
      $macros .= $2;
      $lastr = $r;
    }
    $config .= $c;
  }
  if ($lastr ne $repoid) {
    $macros .= "\n### from $projid\n";
    $macros .= "%_repository $repoid\n";
  }
  if (!@path || $path[0] ne "$projid/$repoid") {
    $macros .= "\n%distribution $distinfo\n";
    $macros .= "%_project $projid\n";
  }
  if ($BSConfig::extramacros) {
    for (sort keys %{$BSConfig::extramacros}) {
      $macros .= $BSConfig::extramacros->{$_} if $projid =~ /$_/;
    }
  }
  if ($BSConfig::extraconfig) {
    my $extraconfig = '';
    for (sort keys %{$BSConfig::extraconfig}) {
      $extraconfig .= $BSConfig::extraconfig->{$_} if $projid =~ /$_/;
    }
    $config .= "\n$extraconfig" if $extraconfig;
  }
  $config .= "\nMacros:\n$macros" if $macros ne '';
  return $config;
}

sub getbuildconfig_ajax {
  my ($cgi, $projid, $repoid) = @_;
  my $jev = $BSServerEvents::gev;
  if (defined $jev->{'getbuildconfig_shared_result'}) {
    print "returning shared result for getbuildconfig $projid/$repoid\n";
    return ($jev->{'getbuildconfig_shared_result'}, 'Content-Type: text/plain');
  }
  $jev->{'getbuildconfig_key'} = "$projid/$repoid";
  my $serialkey = BSSrcServer::Remote::getserialkey('getbuildconfig', $projid);
  my $serial = BSWatcher::serialize($serialkey);
  return undef unless $serial;
  my @path;
  if ($cgi->{'path'}) {
    @path = @{$cgi->{'path'}};
  } else {
    @path = expandsearchpath($projid, $repoid);
    return undef if !@path;
  }
  my $config = concatconfigs($projid, $repoid, undef, @path);
  return undef unless defined $config;
  my @serial_waiting = BSWatcher::serialize_waiting($serialkey);
  for my $waiting (reverse(@serial_waiting)) {
    next if $jev->{'getbuildconfig_key'} ne ($waiting->{'getbuildconfig_key'} || '');
    $waiting->{'getbuildconfig_shared_result'} = $config;
    BSWatcher::serlialize_advance($waiting);
  }
  BSWatcher::serialize_end($serial);
  return ($config, 'Content-Type: text/plain');
}

sub getbuildconfig {
  my ($cgi, $projid, $repoid) = @_;
  return getbuildconfig_ajax($cgi, $projid, $repoid) if $BSStdServer::isajax;
  my @path;
  if ($cgi->{'path'}) {
    @path = @{$cgi->{'path'}};
  } else {
    @path = expandsearchpath($projid, $repoid);
  }
  my $config = concatconfigs($projid, $repoid, undef, @path);
  return ($config, 'Content-Type: text/plain');
}

sub worker_getbuildconfig {
  my ($cgi, $projid, $repoid) = @_;
  if (!$BSStdServer::isajax) {
    local *BSRPC::rpc;
    *BSRPC::rpc = sub {die("noremote operation\n");};
    my ($config) = eval { getbuildconfig($cgi, $projid, $repoid) };
    return ($config, 'Content-Type: text/plain') unless $@;
    die($@) unless $@ eq "noremote operation\n";
  } else {
    return getbuildconfig($cgi, $projid, $repoid);
  }
  BSHandoff::handoff_part('interconnect_out', '/getconfig', undef, BSRPC::args($cgi, 'project', 'repository', 'path', 'workerid'));
}


sub getprojectconfig {
  my ($cgi, $projid) = @_;
  my $proj = checkprojrepoarch($projid, undef, undef, 1);
  if ($proj->{'remoteurl'}) {
    my $config = BSSrcServer::Remote::readconfig_remote($projid, $proj);
    return ($config, 'Content-Type: text/plain');
  }
  my $config;
  if ($cgi->{'rev'}) {
    my $rev = getrev($projid, '_project', $cgi->{'rev'});
    my $files = $rev ? BSRevision::lsrev($rev) : {};
    $config = BSRevision::revreadstr($rev, '_config', $files->{'_config'}, 1) if $files->{'_config'};
  } else {
    $config = readstr("$projectsdir/$projid.conf", 1);
  }
  $config = '' unless defined $config;
  return ($config, 'Content-Type: text/plain');
}

sub putprojectconfig {
  my ($cgi, $projid) = @_;
  my $proj = BSRevision::readproj_local($projid);
  die("$projid is controlled by obs-scm\n") if $proj->{'scmsync'};
  mkdir_p($uploaddir);
  my $uploadfile = "$uploaddir/$$";
  die("upload failed\n") unless BSServer::read_file($uploadfile);
  if (! -s $uploadfile) {
    unlink($uploadfile);
    $uploadfile = undef;
  }
  BSRevision::addrev_local_replace($cgi, $projid, undef, [ $uploadfile, "$projectsdir/$projid.conf", '_config' ]);
  notify_repservers('project', $projid);
  notify("SRCSRV_UPDATE_PROJECT_CONFIG", { "project" => $projid, "sender" => ($cgi->{'user'} || "unknown") });
  return $BSStdServer::return_ok;
}

sub delprojectconfig {
  my ($cgi, $projid) = @_;
  my $proj = BSRevision::readproj_local($projid);
  die("$projid is controlled by obs-scm\n") if $proj->{'scmsync'};
  BSRevision::addrev_local_replace($cgi, $projid, undef, [ undef, "$projectsdir/$projid.conf", '_config' ]);
  notify_repservers('project', $projid);
  notify("SRCSRV_UPDATE_PROJECT_CONFIG", { "project" => $projid, "sender" => ($cgi->{'user'} || "unknown") });
  return $BSStdServer::return_ok;
}

##########################################################################

sub getsources {
  my ($cgi, $projid, $packid, $srcmd5) = @_;
  $packid =~ s/(?<!^_product)(?<!^_patchinfo):.+//;
  my $rev = {'project' => $projid, 'package' => $packid, 'srcmd5' => $srcmd5};
  my $files = BSRevision::lsrev($rev);
  my @files = map {BSRevision::revcpiofile($rev, $_, $files->{$_})} sort keys %$files;
  BSServer::reply_cpio(\@files);
  return undef;
}

my %getfilelist_ajax_inprogress;

sub getfilelist_ajax {
  my ($cgi, $projid, $packid) = @_;

  my $srcmd5 = $cgi->{'rev'};
  if ($cgi->{'view'} eq 'cpio') {
    my $rev = {'project' => $projid, 'package' => $packid, 'srcmd5' => $srcmd5};
    my $files = BSRevision::lsrev($rev);
    my @files = map {BSRevision::revcpiofile($rev, $_, $files->{$_})} sort keys %$files;
    BSWatcher::reply_cpio(\@files);
    return undef;
  }
  if ($cgi->{'view'} eq 'notify') {
    my $jev = $BSServerEvents::gev;
    my $filelist = $jev->{'filelist'};
    my $idstring = "$projid/$packid/$srcmd5";
    if (!$filelist) {
      return $BSStdServer::return_ok if $getfilelist_ajax_inprogress{$idstring};
      $filelist = BSSrcServer::Remote::remote_getrev_getfilelist($projid, $packid, $srcmd5);
      return undef unless defined $filelist;
      # ok, got the filelist, we're the master. get the missing files in the background.
      $jev = BSWatcher::background($BSStdServer::return_ok);
      $getfilelist_ajax_inprogress{$idstring} = $jev;
      $jev->{'filelist'} = $filelist;
      $jev->{'idstring'} = $idstring;
      $jev->{'handler'} = sub {delete $getfilelist_ajax_inprogress{$idstring}};
    }

    # here we're the backgrounded master. get missing files and notify
    my $ret;
    eval {
      $ret = BSSrcServer::Remote::remote_getrev_getfiles($projid, $packid, $srcmd5, $filelist);
    };
    if ($@) {
      # hmm, what to do here? for now we just notify. this will lead to a retry loop, though.
      notify_all_repservers('package', $projid, $packid);
      die($@);
    }
    return undef unless defined $ret;
    delete $getfilelist_ajax_inprogress{$idstring};
    notify_all_repservers('package', $projid, $packid);
    return $ret;
  }
  die("unknown view '$cgi->{'view'}'\n");
}

sub getproductrepositories {
  my ($xml) = @_;

  my @res;
  for my $product (@{$xml->{'products'}->{'product'}}) {
    my @pr;
    for my $repo (@{$product->{'register'}->{'updates'}->{'repository'}}) {
      my ($path) = published_path({}, $repo->{'project'}, $repo->{'name'});
      if (defined($repo->{'url'})) {
        $path = { 'url' => $repo->{'url'} };
      } else {
        if ($path->{'path'}) {
          $path = { 'path' => $path->{'path'} };
        } elsif ($path->{'url'}){
          $path = { 'url' => $path->{'url'} };
        } else {
          $path = {};
        }
      }
      $path->{'arch'} = $repo->{'arch'} if $repo->{'arch'};
      $path->{'zypp'} = $repo->{'zypp'} if $repo->{'zypp'};
      $path->{'debug'} = undef if $repo->{'name'} =~ m/_debug$/;
      $path->{'update'} = undef;
      push @pr, $path;
    }
    for my $repo (@{$product->{'register'}->{'pool'}->{'repository'}}) {
      my $path;
      if (defined($repo->{'url'})) {
        $path = { 'url' => $repo->{'url'} };
      } else {
        ($path) = published_path({ 'medium' => $repo->{'medium'} }, $repo->{'project'}, $repo->{'name'});
        if ($path->{'path'}) {
          $path = { 'path' => $path->{'path'} };
        } elsif ($path->{'url'}){
          $path = { 'url' => $path->{'url'} };
        } else {
          $path = {};
        }
      }
      $path->{'arch'} = $repo->{'arch'} if $repo->{'arch'};
      $path->{'zypp'} = $repo->{'zypp'} if $repo->{'zypp'};
      $path->{'debug'} = undef if $repo->{'medium'} && $repo->{'medium'} =~ m/_debug$/;
      push @pr, $path;
    }
    my $prod = { 'name' => $product->{'name'}, 'repository' => \@pr };
    $prod->{'distrotarget'} = $product->{'register'}->{'updates'}->{'distrotarget'} if $product->{'register'}->{'updates'}->{'distrotarget'};
    push @res, $prod;
  }
  return @res;
}

sub getfilelist {
  my ($cgi, $projid, $packid) = @_;

  my $view = $cgi->{'view'};
  my $rev;
  my $linked;
  $linked = [] if $cgi->{'withlinked'};
  my $islatestrev;
  if ($cgi->{'meta'}) {
    $rev = BSRevision::getrev_meta($projid, $packid, $cgi->{'rev'}, $cgi->{'deleted'});
  } elsif ($cgi->{'deleted'}) {
    $rev = BSRevision::getrev_local($projid, $packid, $cgi->{'rev'}, $cgi->{'deleted'});
  } else {
    $rev = getrev($projid, $packid, defined($cgi->{'rev'}) ? $cgi->{'rev'} : 'upload', $linked);
    $islatestrev = 1 if !defined($cgi->{'rev'}) && $rev->{'srcmd5'} && $rev->{'srcmd5'} ne 'pattern' && $rev->{'srcmd5'} ne 'upload';
  }
  my $li = {};
  my $files = BSRevision::lsrev($rev, $li);

  # show state of current source service run, if defined
  my $serviceinfo;
  if ($files->{'_link'} && $cgi->{'emptylink'}) {
    # ignore all service runs if emptylink is used
    delete $li->{'xservicemd5'};
    delete $li->{'lservicemd5'};
    delete $li->{'lxservicemd5'};
  } elsif ($li->{'xservicemd5'} || $li->{'lservicemd5'}) {
    # new style
    $serviceinfo = {};
    $serviceinfo->{'lsrcmd5'} = $li->{'lservicemd5'} if $li->{'lservicemd5'};
    # $serviceinfo->{'lxsrcmd5'} = $li->{'lxservicemd5'} if $li->{'lxservicemd5'};
    if ($li->{'xservicemd5'}) {
      if ($cgi->{'expand'}) {
	$serviceinfo->{'lsrcmd5'} = $rev->{'srcmd5'};
	my $sli = {};
	$files = BSSrcServer::Service::handleservice($rev, $files, $li->{'xservicemd5'}, $sli);
	$serviceinfo->{'code'} = 'succeeded';	# otherwise it already died...
	# $serviceinfo->{'lxsrcmd5'} = $sli->{'lxservicemd5'} if $sli->{'lxservicemd5'};
      } else {
        eval { BSSrcServer::Service::handleservice({ %$rev }, $files, $li->{'xservicemd5'}) };
	my $error = $@;
	chomp $error if $error;
	if (!$error) {
	  $serviceinfo->{'code'} = 'succeeded';
	  $serviceinfo->{'xsrcmd5'} = $li->{'xservicemd5'};
	} elsif ($error eq 'service in progress') {
	  $serviceinfo->{'code'} = 'running';
	} else {
	  $serviceinfo->{'code'} = 'failed';
	  $serviceinfo->{'xsrcmd5'} = $li->{'xservicemd5'};
	  $serviceinfo->{'error'} = $error;
	}
      }
    }
    delete $li->{'xservicemd5'};
    delete $li->{'lservicemd5'};
    delete $li->{'lxservicemd5'};
  } elsif ($BSConfig::old_style_services && $files->{'_service'} && $packid ne '_project' && !$cgi->{'meta'} && !defined($cgi->{'rev'})) {
    # check error/in progress
    $serviceinfo = {};
    my $lockfile = "$eventdir/service/${projid}::$packid";
    if (-e $lockfile) {
      $serviceinfo->{'code'} = 'running';
    } elsif ($files->{'_service_error'}) {
      $serviceinfo->{'code'} = 'failed';
      $serviceinfo->{'error'} = BSRevision::revreadstr($rev, '_service_error', $files->{'_service_error'});
    } else {
      $serviceinfo->{'code'} = 'succeeded';
    }
  } elsif ($files->{'_service_error'}) {
    $serviceinfo = {'code' => 'failed'};
    $serviceinfo->{'error'} = BSRevision::revreadstr($rev, '_service_error', $files->{'_service_error'});
  }
  undef $islatestrev if $serviceinfo && !$cgi->{'expand'};

  if ($files->{'_link'}) {
    if ($cgi->{'emptylink'}) {
      my $l = BSRevision::revreadxml($rev, '_link', $files->{'_link'}, $BSXML::link);
      delete $l->{'patches'};
      mkdir_p($uploaddir);
      writexml("$uploaddir/$$", undef, $l, $BSXML::link);
      $files = {};
      $files->{'_link'} = BSSrcrep::addfile($projid, $packid, "$uploaddir/$$", '_link');
      $rev = addrev({}, $projid, $packid, $files, '');
      undef $islatestrev;
    }
    my %lrev = %$rev;
    $lrev{'linkrev'} = $cgi->{'linkrev'} if $cgi->{'linkrev'};
    $li->{'linked'} = $linked if $linked;
    my $lfiles = BSSrcServer::Link::handlelinks(\%lrev, $files, $li);
    BSSrcServer::Multibuild::updatemultibuild($projid, $packid, $lfiles) if $islatestrev && !$cgi->{'linkrev'} && ref($lfiles);
    if ($cgi->{'expand'}) {
      if (!ref($lfiles)) {
	if ($cgi->{'withlinked'} && !$view) {
	  my $ret = {};
	  $ret->{'name'} = $packid;
	  $ret->{'error'} = $lfiles || 'internal error';
	  $ret->{'linkinfo'} = $li;
	  return ($ret, $BSXML::dir);
	}
	die("$lfiles\n");
      }
      $files = $lfiles;
      %$rev = %lrev;
      $rev->{'rev'} = $rev->{'srcmd5'};
    } else {
      if (ref $lfiles) {
        $li->{'xsrcmd5'} = $lrev{'srcmd5'};
      } else {
	# link is broken
	$li->{'error'} = $lfiles;
	# set xsrcmd5 if we have a link error file
	$li->{'xsrcmd5'} = $lrev{'srcmd5'} if $lrev{'srcmd5'} && BSSrcrep::havelinkerror($lrev{'project'}, $lrev{'package'}, $lrev{'srcmd5'});
	if ($cgi->{'lastworking'}) {
	  my $lastworking = BSSrcServer::Link::findlastworkinglink($rev);
	  $li->{'lastworking'} = $lastworking if $lastworking;
	}
      }
    }
  } else {
    BSSrcServer::Multibuild::updatemultibuild($projid, $packid, $files) if $islatestrev;
  }
  if ($islatestrev && $rev->{'originpackage'}) {
    my $mb = BSSrcServer::Multibuild::getmultibuild($projid, $rev->{'package'}) || {};
    die("404 package '$packid' does not exist\n") unless grep {"$rev->{'package'}:$_" eq $packid} @{$mb->{'flavor'} || $mb->{'package'} || []};
  }

  if ($cgi->{'extension'}) {
    for (keys %$files) {
      delete $files->{$_} unless /\.\Q$cgi->{'extension'}\E$/;
    }
  }

  if ($view && $view eq 'cpio') {
    if (!$cgi->{'extension'} && $rev->{'srcmd5'} && $rev->{'srcmd5'} ne 'upload' && $rev->{'srcmd5'} ne 'pattern' && $rev->{'srcmd5'} ne 'empty' && $rev->{'srcmd5'} ne $BSSrcrep::emptysrcmd5) {
      # hack: we identify remote source downloads by looking at the user agent
      my $useragent = $BSServer::request->{'headers'}->{'user-agent'} || '';
      if ($useragent =~ /BSRPC/) {
	BSHandoff::handoff_part('interconnect_in', "/source/$projid/$rev->{'package'}", undef, "rev=$rev->{'srcmd5'}", 'view=cpio');
      }
    }
    my @files = map {BSRevision::revcpiofile($rev, $_, $files->{$_})} sort keys %$files;
    BSServer::reply_cpio(\@files);
    return undef;
  }

  if ($view && ($view eq 'products' || $view eq 'productrepositories') ) {
    my @res;
    my $reader = sub { return BSRevision::revreadstr($rev, $_[0], $files->{$_[0]}) };
    for my $filename (sort keys %$files) {
      next unless $filename =~ /\.product$/s;
      next if $cgi->{'product'} && $filename ne "$cgi->{'product'}.product";
      my $xml = BSProductXML::readproductxml([$reader, $filename], 1);
      die("400 Unable to parse $filename\n") unless $xml;
      push @res, $xml;
    }
    if ($view eq 'productrepositories') {
      @res = map {getproductrepositories($_)} @res;
      return ({"product" => \@res}, $BSProductXML::productlistrepositories);
    }
    return ({'productdefinition' => \@res}, $BSProductXML::products);
  }

  my $ret = {};
  $ret->{'name'} = $packid;
  $ret->{'srcmd5'} = $rev->{'srcmd5'} if $rev->{'srcmd5'} ne 'empty';
  $ret->{'rev'} = $rev->{'rev'} if exists $rev->{'rev'};
  $ret->{'vrev'} = $rev->{'vrev'} if exists $rev->{'vrev'};
  $ret->{'serviceinfo'} = $serviceinfo if $serviceinfo;
  my @res;
  for my $filename (sort keys %$files) {
    my @s = BSRevision::revstat($rev, $filename, $files->{$filename});
    if (@s) {
      push @res, {'name' => $filename, 'md5' => $files->{$filename}, 'size' => $s[7], 'mtime' => $s[9]};
    } else {
      push @res, {'name' => $filename, 'md5' => $files->{$filename}, 'error' => "$!"};
    }
  }
  if (%$li) {
    BSSrcServer::Link::linkinfo_addtarget($rev, $li);
    $ret->{'linkinfo'} = $li;
  }

  # fake linkinfo element for project links. see comment in remote_getrev
  if ($linked && @$linked && !$ret->{'linkinfo'}) {
    $li->{'linked'} = $linked;
    $ret->{'linkinfo'} = $li;
  }

  $ret->{'entry'} = \@res;
  return ($ret, $BSXML::dir);
}

sub getfile {
  my ($cgi, $projid, $packid, $filename) = @_;
  die("no filename\n") unless defined($filename) && $filename ne '';
  die("bad filename\n") if $filename =~ /\// || $filename =~ /^\./;
  my $rev;
  if ($cgi->{'meta'}) {
    $rev = BSRevision::getrev_meta($projid, $packid, $cgi->{'rev'}, $cgi->{'deleted'});
  } elsif ($cgi->{'deleted'}) {
    $rev = BSRevision::getrev_local($projid, $packid, $cgi->{'rev'}, $cgi->{'deleted'});
  } else {
    $rev = getrev($projid, $packid, defined($cgi->{'rev'}) ? $cgi->{'rev'} : 'upload');
  }
  my $view = $cgi->{'view'} || '';
  if ($view eq 'blame') {
    die("cannot blame deleted packages\n") if $cgi->{'deleted'};
    my $b = BSSrcServer::Blame::blame($rev, $filename, $cgi->{'expand'}, $cgi->{'meta'});
    my $ret = '';
    my %links;
    my $linkno = 1;
    $links{"$projid/$packid"} = 0;
    for my $rev (@$b) {
      next unless ref($rev);
      next if exists $links{"$rev->{'project'}/$rev->{'package'}"};
      $ret .= sprintf("%7s ", "$linkno:")."$rev->{'project'}/$rev->{'package'}\n";
      $links{"$rev->{'project'}/$rev->{'package'}"} = $linkno++;
    }
    $ret .= "\n" if $linkno > 1;
    my $li = 1;
    while (@$b) {
      my ($rev, $line) = splice(@$b, 0, 2);
      if ($linkno > 1) {
        my $lno = $links{"$rev->{'project'}/$rev->{'package'}"};
        $lno = $lno ? "$lno:" : '';
        $ret .= sprintf("%7s ", "$lno$rev->{'rev'}");
      } else {
        $ret .= sprintf("%4d ", $rev->{'rev'});
      }
      my $user = sprintf("%-12s", $rev->{'user'});
      my $date = BSUtil::isotime($rev->{'time'});
      $ret .= "($user $date ".sprintf("%5d", $li++).") $line\n";
    }
    return $ret;
  } elsif ($view) {
    die("unknown view '$view'\n");
  }

  my $files;
  if ($cgi->{'expand'}) {
    $files = lsrev_expanded($rev);
  } else {
    $files = BSRevision::lsrev($rev);
  }
  if (!$cgi->{'meta'} && !$cgi->{'deleted'} && !$cgi->{'rev'} && !$files->{$filename} && $filename eq '_serviceerror') {
    my $pack = BSRevision::readpack_local($projid, $packid, 1);
    if ($pack && $pack->{'scmsync'}) {
      $rev = BSRevision::getrev_local($projid, $packid, BSSrcServer::Service::genservicemark_obsscm($projid, $packid));
      $files = BSRevision::lsrev($rev);
    }
  }
  die("404 $filename: no such file\n") unless $files->{$filename};
  my @s = BSRevision::revstat($rev, $filename, $files->{$filename});
  die("$projid/$packid/$files->{$filename}-$filename: $!\n") unless @s;
  if (!$cgi->{'noajax'} && !$BSStdServer::isajax && $rev->{'srcmd5'} && $rev->{'srcmd5'} ne 'upload' && $rev->{'srcmd5'} ne 'pattern' && $rev->{'srcmd5'} ne 'empty' && $rev->{'srcmd5'} ne $BSSrcrep::emptysrcmd5) {
    # hack: we identify remote source downloads by looking at the user agent
    my $useragent = $BSServer::request->{'headers'}->{'user-agent'} || '';
    if ($useragent =~ /BSRPC/) {
      BSHandoff::handoff_part('interconnect_in', "/source/$projid/$packid/$filename", undef, "rev=$rev->{'srcmd5'}");
    }
  }
  my $fd = gensym;
  BSRevision::revopen($rev, $filename, $files->{$filename}, $fd) || die("$projid/$packid/$files->{$filename}-$filename: $!\n");
  BSWatcher::reply_file($fd);
  return undef;
}

sub putfile {
  my ($cgi, $projid, $packid, $filename) = @_;
  die("no filename\n") unless defined($filename) && $filename ne '';
  die("bad filename\n") if $filename =~ /\// || $filename =~ /^\./;
  mkdir_p($uploaddir);
  my $uploaded = BSServer::read_file("$uploaddir/$$", 'withmd5' => 1);
  die("upload failed\n") unless $uploaded;
  if ($cgi->{'meta'}) {
    die("404 project '$projid' does not exist\n") unless -e "$projectsdir/$projid.xml";
    if ($filename eq '_attribute') {
      my $attribs = readxml("$uploaddir/$$", $BSXML::attributes);
      BSVerify::verify_attributes($attribs);
      writexml("$uploaddir/$$", undef, $attribs, $BSXML::attributes);
    } elsif ($filename eq '_frozenlinks') {
      my $frozenx = readxml("$uploaddir/$$", $BSXML::frozenlinks);
      BSVerify::verify_frozenlinks($frozenx);
      writexml("$uploaddir/$$", undef, $frozenx, $BSXML::frozenlinks);
    } else {
      die("unsupported meta operation\n");
    }
    my $rev = BSRevision::addrev_meta_replace($cgi, $projid, $packid, [ "$uploaddir/$$", undef, $filename ]);
    notify_repservers('package', $projid) if $filename eq '_frozenlinks';
    notify("SRCSRV_UPLOAD", {project => $projid, package => $packid, filename => $filename,
                             meta => 1, rev => $rev, user => ($cgi->{'user'} || "unknown")});
    delete $rev->{'project'};
    delete $rev->{'package'};
    return ($rev, $BSXML::revision);
  }

  my $rev = getrev($projid, $packid, defined($cgi->{'rev'}) ? $cgi->{'rev'} : 'upload');
  die("file '$filename' is read-only\n") if ($filename =~ /^_service:/) && !$cgi->{'force'};
  BSSrcrep::addfile($projid, $packid, "$uploaddir/$$", $filename, $uploaded->{'md5'});
  # create new meta file
  my $files;
  if ($cgi->{'keeplink'}) {
    $files = lsrev_expanded($rev);
  } else {
    $files = BSRevision::lsrev($rev);
  }
  $files->{$filename} = $uploaded->{'md5'};
  $files = BSSrcServer::Link::keeplink($cgi, $projid, $packid, $files) if $cgi->{'keeplink'};
  $rev = addrev_runservice($cgi, $projid, $packid, $files, $cgi->{'rev'});
  delete $rev->{'project'};
  delete $rev->{'package'};
  return ($rev, $BSXML::revision);
}

sub getsourcediffcache {
  my ($cgi, $cacheid) = @_;

  my $view = $cgi->{'view'} || '';
  my $cn = "$diffcache/".substr($cacheid, 0, 2)."/$cacheid";
  BSWatcher::addfilewatcher($cn) if $BSStdServer::isajax;
  my $lockc = BSUtil::lockcheck('>>', "$cn.run");
  my $fd = gensym;
  if (open($fd, '<', $cn)) {
    unlink("$cn.run");
    utime(time, time, $cn);
    return do_diff_file_filter_reply($cgi, $fd) if $view eq 'xml' && $cgi->{'file'};
    BSWatcher::reply_file($fd, $view eq 'xml' ? 'Content-Type: text/xml' : 'Content-Type: text/plain');
    return undef;
  }
  return undef if $BSStdServer::isajax && !$lockc;
  die("cache entry '$cacheid' does not exist\n");
}

sub cleandiffcache {
  my ($cacheid, $thr) = @_;
  my $now = time();
  my $slot = substr($cacheid, 0, 2);
  my $dir = "$diffcache/$slot";
  my $cnt = 0;
  for my $e (sort(ls($dir))) {
     next unless $e =~ /^[0-9a-f]{32}$/;
     my @s = stat("$dir/$e");
     next if $s[9] > $now - $thr;
     unlink("$dir/$e");
     $cnt++;
  }
  print "cleandiffcache: removed $cnt entries in slot $slot\n";
}

# this modifies xmlret in-place!
sub do_diff_file_filter {
  my ($xmlret, $filefilter) = @_;
  return $xmlret unless $filefilter && $xmlret->{'files'} && $xmlret->{'files'}->{'file'};
  my %file = map {$_ => 1} @$filefilter;
  for my $f (splice @{$xmlret->{'files'}->{'file'}}) {
    next unless ($f->{'old'} && $file{$f->{'old'}->{'name'}}) || ($f->{'new'} && $file{$f->{'new'}->{'name'}});
    push @{$xmlret->{'files'}->{'file'}}, $f;
  }
  return $xmlret;
}

sub do_diff_file_filter_reply {
  my ($cgi, $fd) = @_;
  my $xmlret = '';
  1 while sysread($fd, $xmlret, 8192, length($xmlret));
  close $fd;
  $xmlret = BSUtil::fromxml($xmlret, $BSXML::sourcediff);
  my $d = BSUtil::toxml(do_diff_file_filter($xmlret, $cgi->{'file'}), $BSXML::sourcediff);
  BSWatcher::reply($d, 'Content-Type: text/xml');
  return undef;
}

sub sourcediff {
  my ($cgi, $projid, $packid) = @_;

  BSVerify::verify_linkrev($cgi->{'olinkrev'}) if defined($cgi->{'olinkrev'}) && $cgi->{'olinkrev'} ne 'linkrev';
  my $oprojid = exists($cgi->{'oproject'}) ? $cgi->{'oproject'} : $projid;
  my $opackid = exists($cgi->{'opackage'}) ? $cgi->{'opackage'} : $packid;

  my $fmax = 200;
  my $tmax = 16000;
  $fmax = $cgi->{'filelimit'} if defined $cgi->{'filelimit'};
  $tmax = $cgi->{'tarlimit'} if defined $cgi->{'tarlimit'};
  undef $fmax unless $fmax;
  undef $tmax unless $tmax;

  my $have0rev = (defined($cgi->{'rev'}) && $cgi->{'rev'} eq '0') || (defined($cgi->{'orev'}) && $cgi->{'orev'} eq '0');
  my $rev;
  if ($cgi->{'meta'}) {
    $rev = BSRevision::getrev_meta($projid, $packid, $cgi->{'rev'});
  } else {
    $rev = getrev($projid, $packid, defined($cgi->{'rev'}) ? $cgi->{'rev'} : 'upload', undef, $cgi->{'missingok'});
  }
  my $linkinfo = {};
  my $files = BSRevision::lsrev($rev, $linkinfo);
  $files = BSSrcServer::Service::handleservice($rev, $files, $linkinfo->{'xservicemd5'}) if $cgi->{'expand'} && $linkinfo->{'xservicemd5'};
  my $orev = $cgi->{'orev'};
  if (!defined($cgi->{'oproject'}) && !defined($cgi->{'opackage'}) && !defined($cgi->{'orev'}) && $rev->{'rev'}) {
    die("revision is not a simple commit\n") unless $rev->{'rev'} =~ /^\d+$/s;
    $orev = $rev->{'rev'} - 1;
    $have0rev = 1 if $orev == 0;
    $cgi->{'olinkrev'} = 'linkrev' if !defined($cgi->{'olinkrev'});
  }
  if ($cgi->{'meta'}) {
    $orev = BSRevision::getrev_meta($oprojid, $opackid, $orev);
  } else {
    $orev = getrev($oprojid, $opackid, defined($orev) ? $orev : 'latest', undef, $cgi->{'missingok'});
  }
  my $olinkinfo = {};
  my $ofiles = BSRevision::lsrev($orev, $olinkinfo);
  $ofiles = BSSrcServer::Service::handleservice($orev, $ofiles, $olinkinfo->{'xservicemd5'}) if $cgi->{'expand'} && $olinkinfo->{'xservicemd5'};
  if ($cgi->{'expand'} || (!$have0rev && $files->{'_link'} && !$ofiles->{'_link'}) || (!$have0rev && $ofiles->{'_link'} && !$files->{'_link'})) {
    # expand links
    if ($files->{'_link'}) {
      $rev->{'linkrev'} = $cgi->{'linkrev'} if $cgi->{'linkrev'};
      my %li;
      my $l = BSRevision::revreadxml($rev, '_link', $files->{'_link'}, $BSXML::link, 1);
      if ($l) {
        $l->{'project'} = $rev->{'project'} unless defined $l->{'project'};
        $l->{'package'} = $rev->{'package'} unless defined $l->{'package'};
      }
      $files = BSSrcServer::Link::handlelinks($rev, $files, \%li);
      die("bad link: $files\n") unless ref $files;

      # some nasty magic to improve diff usability
      if ($l && $cgi->{'linkrev'} && $l->{'project'} eq $oprojid && $l->{'package'} eq $opackid && !$l->{'rev'} && !$cgi->{'orev'}) {
        # we're diffing against the link target. As the user specified a baserev, we should use it
        # instead of the latest source
        $orev = getrev($oprojid, $opackid, $li{'srcmd5'});
        $ofiles = BSRevision::lsrev($orev);
      }
      # olinkrev=linkrev: reuse same linkrev if the link target matches
      if ($cgi->{'olinkrev'} && $cgi->{'olinkrev'} eq 'linkrev' && $ofiles->{'_link'}) {
	my $ol = BSRevision::revreadxml($orev, '_link', $ofiles->{'_link'}, $BSXML::link, 1);
	if ($ol) {
	  $ol->{'project'} = $orev->{'project'} unless defined $ol->{'project'};
	  $ol->{'package'} = $orev->{'package'} unless defined $ol->{'package'};
	}
	$cgi->{'olinkrev'} = $li{'srcmd5'} if $l && $ol && $l->{'project'} eq $ol->{'project'} && $l->{'package'} eq $ol->{'package'};
      }
    }
    if ($ofiles->{'_link'}) {
      $orev->{'linkrev'} = $cgi->{'olinkrev'} if $cgi->{'olinkrev'} && $cgi->{'olinkrev'} ne 'linkrev';
      $ofiles = BSSrcServer::Link::handlelinks($orev, $ofiles);
      die("bad link: $ofiles\n") unless ref $ofiles;
    }
  }
  my $view = $cgi->{'view'} || '';
  $view = 'unified' if $cgi->{'unified'};
  die("unsupported view '$view'\n") if $view && ($view ne 'xml' && $view ne 'unified');
  my $cacheid = "//cacheversion:2/";
  $cacheid .= "$orev->{'srcmd5'}/$rev->{'srcmd5'}";
  $cacheid .= "/unified:1" if $view && $view eq 'unified';
  $cacheid .= "/view:$cgi->{'view'}" if $view && $view ne 'unified';
  $cacheid .= "/fmax:$fmax" if defined $fmax;
  $cacheid .= "/tmax:$tmax" if defined $tmax;
  $cgi->{'withissues'} = 1 if $cgi->{'onlyissues'};
  if ($cgi->{'withissues'}) {
    my @s = stat("$BSConfig::bsdir/issuetrackers.xml");
    $cacheid .= "/withissues:$s[9]/$s[7]/$s[1]" if @s;
    $cacheid .= "/onlyissues" if $cgi->{'onlyissues'};
  }
  # we always add package names to guard against srcrep aliasing issues
  $cacheid .= "/opackage:$orev->{'package'}/package:$rev->{'package'}";
  if ($view eq 'xml') {
    # xml output unfortunately contains project, package, and revision
    $cacheid .= "/oproject:$orev->{'project'}/project:$rev->{'project'}";
    $cacheid .= "/orev:$orev->{'rev'}" if defined $orev->{'rev'};
    $cacheid .= "/rev:$rev->{'rev'}" if defined $rev->{'rev'};
  } elsif ($view eq 'unified') {
    # unified output contains the revision
    $cacheid .= "/orev:$orev->{'rev'}" if defined $orev->{'rev'};
    $cacheid .= "/rev:$rev->{'rev'}" if defined $rev->{'rev'};
  }
  my $need_file_filter_postprocess;
  if ($cgi->{'file'}) {
    my %file;
    for (@{$cgi->{'file'}}) {
      if (/^([^\/]+)\//) {
	$file{$1} = 1;
	$need_file_filter_postprocess = 1;
      } else {
	$file{$_} = 1;
      }
    }
    die("in-archive file diff filtering only works for xml output\n") if $need_file_filter_postprocess && $view ne 'xml';
    # extend file filter with renamed files
    my $similar = $cgi->{'nodiff'} || $cgi->{'unified'} ? 0 : 1;
    my $sim = $similar ? BSSrcServer::Srcdiff::findsim($ofiles, $files) : {};
    for (grep {defined $sim->{$_}} keys %$files) {
      $file{$_} = $file{$sim->{$_}} = 1 if $file{$_} || $file{$sim->{$_}};
    }
    $cacheid .= "/file:$_" for sort keys %file;
    for (keys %$ofiles) {
      delete $ofiles->{$_} unless $file{$_};
    }
    for (keys %$files) {
      delete $files->{$_} unless $file{$_};
    }
  }
  $cacheid = Digest::MD5::md5_hex($cacheid);
  my $xmlret;
  if ($view eq 'xml') {
    $xmlret = {};
    $xmlret->{'key'} = $cacheid;
    $rev->{'rev'} ||= 0;
    $rev->{'srcmd5'} = $BSSrcrep::emptysrcmd5 if $rev->{'srcmd5'} eq 'empty';
    $orev->{'rev'} ||= 0;
    $orev->{'srcmd5'} = $BSSrcrep::emptysrcmd5 if $rev->{'srcmd5'} eq 'empty';
    $xmlret->{'old'} = { 'project' => $orev->{'project'}, 'package' => $orev->{'package'}, 'rev' => $orev->{'rev'}, 'srcmd5' => $orev->{'srcmd5'} };
    $xmlret->{'new'} = { 'project' => $rev->{'project'}, 'package' => $rev->{'package'}, 'rev' => $rev->{'rev'}, 'srcmd5' => $rev->{'srcmd5'} };
    $xmlret->{'files'} = {};
  }
  if ($packid eq $opackid) {
    if (!grep {($ofiles->{$_} || '') ne ($files->{$_} || '')} (keys %$ofiles, keys %$files)) {
      # all files identical, don't bother
      return ($xmlret, $BSXML::sourcediff) if $view eq 'xml';
      return ('', 'Content-Type: text/plain');
    }
  }
  $cgi->{'nocache'} = 1 if $cgi->{'nodiff'};	# do not cache nodiff, they are fast
  local *F;
  local *LF;
  my $cn;
  if (!$cgi->{'nocache'}) {
    $cn = "$diffcache/".substr($cacheid, 0, 2)."/$cacheid";
    if (open(F, '<', $cn)) {
      utime(time, time, $cn);
      return do_diff_file_filter_reply($cgi, \*F) if $need_file_filter_postprocess;
      BSServer::reply_file(\*F, $view eq 'xml' ? 'Content-Type: text/xml' : 'Content-Type: text/plain');
      return undef;
    }
    die("412 diff not yet in cache\n") if $cgi->{'cacheonly'};
    mkdir_p("$diffcache/".substr($cacheid, 0, 2));
    if (!BSUtil::lockcheck('>>', "$cn.run")) {
      # somebody is in the process of creating this, hand over to ajax and wait for the result
      my @args;
      push @args, "view=$view" if $view;
      push @args, map {"file=$_"} @{$cgi->{'file'} || []} if $need_file_filter_postprocess;
      BSHandoff::handoff("/sourcediffcache/$cacheid", undef, @args);
    }
    BSUtil::lockopen(\*LF, '>>', "$cn.run");
    # retry open, maybe somebody else has created the diff meanwhile
    if (open(F, '<', $cn)) {
      unlink("$cn.run");
      close LF;
      utime(time, time, $cn);
      return do_diff_file_filter_reply($cgi, \*F) if $need_file_filter_postprocess;
      BSServer::reply_file(\*F, $view eq 'xml' ? 'Content-Type: text/xml' : 'Content-Type: text/plain');
      return undef;
    }
  }
  my $tmpdir = "$uploaddir/srcdiff$$";
  my $d;
  my %xobscpio;
  mkdir_p($uploaddir);
  my $xobscpio = sub {
    return BSRevision::revfilename($_[0], $_[1], $_[2]) if $_[3] || $_[1] !~ /\.obscpio$/;
    my $tmp = "$uploaddir/sourcediff.obscpio.$_[0]->{'package'}.$$.$_[2]-$_[1]";
    BSSrcrep::copyonefile_tmp($_[0]->{'project'}, $_[0]->{'package'}, $_[1], $_[2], $tmp) unless $xobscpio{$tmp};
    $xobscpio{$tmp} = 1;
    return $tmp;
  };
  my $ofn = sub { $xobscpio->($orev, $_[0], $ofiles->{$_[0]}, $_[1]) };
  my $fn  = sub { $xobscpio->($rev, $_[0], $files->{$_[0]}, $_[1]) };
  my %opts = ('edir' => $tmpdir, 'fmax' => $fmax, 'tmax' => $tmax, 'tfmax' => $fmax, 'doarchive' => 1, 'similar' => 1);
  if ($cgi->{'nodiff'}) {
    delete $opts{'doarchive'};
    delete $opts{'similar'};
    $opts{'nodiff'} = 1;
  }
  if ($view eq 'xml') {
    if (!$cgi->{'onlyissues'}) {
      $xmlret->{'files'} = { 'file' => BSSrcServer::Srcdiff::datadiff($ofn, $ofiles, $fn, $files, %opts) };
    }
    if ($cgi->{'withissues'}) {
      my $trackers = readxml("$BSConfig::bsdir/issuetrackers.xml", $BSXML::issue_trackers, 1) || {};
      $opts{'trackers'} = $trackers->{'issue-tracker'} || [];
      $xmlret->{'issues'} = { 'issue' => BSSrcServer::Srcdiff::issuediff($ofn, $ofiles, $fn, $files, %opts) };
    }
    BSUtil::data2utf8xml($xmlret);
    $d = BSUtil::toxml($xmlret, $BSXML::sourcediff);
  } elsif ($view eq 'unified') {
    delete $opts{'doarchive'};
    delete $opts{'similar'};
    $opts{'nodecomp'} = 1;
    $opts{'oldrevision'} = $orev->{'rev'} if defined $orev->{'rev'};
    $opts{'newrevision'} = $rev->{'rev'} if defined $rev->{'rev'};
    $d = BSSrcServer::Srcdiff::unifieddiff($ofn, $ofiles, $fn, $files, %opts);
  } else {
    $d = BSSrcServer::Srcdiff::srcdiff($ofn, $ofiles, $fn, $files, %opts);
  }
  unlink($_) for keys %xobscpio;
  if ($cn) {
    mkdir_p("$diffcache/".substr($cacheid, 0, 2));
    writestr("$diffcache/.new$$", $cn, $d);
    unlink("$cn.run");
    close LF;
  }
  $d = BSUtil::toxml(do_diff_file_filter($xmlret, $cgi->{'file'}), $BSXML::sourcediff) if $need_file_filter_postprocess;
  BSServer::reply($d, $view eq 'xml' ? 'Content-Type: text/xml' : 'Content-Type: text/plain');
  undef $d;
  BSServer::done(1);
  cleandiffcache($cacheid, 86400 * 100) if $cn && substr($cacheid, -2, 2) eq '00';
  return undef;
}

sub linkdiff {
  my ($cgi, $projid, $packid) = @_;
  my $rev = getrev($projid, $packid, defined($cgi->{'rev'}) ? $cgi->{'rev'} : 'upload');
  $rev->{'linkrev'} = $cgi->{'linkrev'} if $cgi->{'linkrev'};
  my $linkinfo = {};
  my $files = lsrev_expanded($rev, $linkinfo);
  die("not a link\n") unless $linkinfo->{'srcmd5'};
  BSSrcServer::Link::linkinfo_addtarget($rev, $linkinfo);
  return sourcediff({
    %$cgi, 'expand' => 0,
    'oproject' => $linkinfo->{'project'},
    'opackage' => $linkinfo->{'package'},
    'orev' => $linkinfo->{'srcmd5'},
    'missingok' => $linkinfo->{'missingok'},
    'rev' => $rev->{'srcmd5'},
  }, $projid, $packid);
}

sub servicediff {
  my ($cgi, $projid, $packid) = @_;
  die("servicediff only works for new style services\n") if $BSConfig::old_style_services;
  my $rev = getrev($projid, $packid, defined($cgi->{'rev'}) ? $cgi->{'rev'} : 'upload');
  my $linkinfo = {};
  my $files = BSRevision::lsrev($rev, $linkinfo);
  if ($linkinfo->{'xservicemd5'}) {
    return sourcediff({%$cgi, 'expand' => 0, 'orev' => $rev->{'srcmd5'}, 'rev' => $linkinfo->{'xservicemd5'}}, $projid, $packid);
  } elsif ($linkinfo->{'lservicemd5'}) {
    return sourcediff({%$cgi, 'expand' => 0, 'orev' => $linkinfo->{'lservicemd5'}, 'rev' => $rev->{'srcmd5'}}, $projid, $packid);
  } else {
    die("no service was run for this revision\n");
  }
}

sub sourcecommit {
  my ($cgi, $projid, $packid) = @_;
  my $rev = getrev($projid, $packid, defined($cgi->{'rev'}) ? $cgi->{'rev'} : 'upload');
  my $files = BSRevision::lsrev($rev);
  $files = BSSrcServer::Link::keeplink($cgi, $projid, $packid, $files) if $cgi->{'keeplink'};
  $rev = addrev_runservice($cgi, $projid, $packid, $files);
  delete $rev->{'project'};
  delete $rev->{'package'};
  return ($rev, $BSXML::revision);
}

sub sourcecommitfilelist {
  my ($cgi, $projid, $packid) = @_;
  BSVerify::verify_md5($cgi->{'servicemark'}) if $cgi->{'servicemark'};
  mkdir_p($uploaddir);
  my $uploaded = BSServer::read_file("$uploaddir/$$");
  die("upload failed\n") unless $uploaded;
  my $fl = readxml("$uploaddir/$$", $BSXML::dir);
  unlink("$uploaddir/$$");
  # make sure we know every file
  my @missing;
  my $files = {};
  my $ofiles = {};
  my $ofiles_expanded = {};
  my $orev = {'project' => $projid, 'package' => $packid};
  if ($cgi->{'withvalidate'}) {
    eval {
      my $rev_old = getrev($projid, $packid);
      $ofiles = BSRevision::lsrev($rev_old);
      $ofiles_expanded = lsrev_expanded($rev_old);
    };
  }
  for my $entry (@{$fl->{'entry'} || []}) {
    BSVerify::verify_filename($entry->{'name'});
    BSVerify::verify_md5($entry->{'md5'});
    if (! -e BSRevision::revfilename($orev, $entry->{'name'}, $entry->{'md5'})) {
      $entry->{'hash'} = 'new' if $cgi->{'withvalidate'};
      push @missing, $entry;
    } else {
      die("duplicate file: $entry->{'name'}\n") if exists $files->{$entry->{'name'}};
      if ($entry->{'hash'}) {
        my $fd = gensym;
        BSRevision::revopen($orev, $entry->{'name'}, $entry->{'md5'}, $fd);
        my $sha256 = Digest::SHA->new(256);
        my $hash_to_check = "sha256:" . $sha256->addfile($fd)->hexdigest;
        if ($hash_to_check ne $entry->{'hash'}) {
          die("SHA mismatch for same md5sum in $packid for file $entry->{'name'} with sum $entry->{'md5'}\n");
        }
      } elsif ($cgi->{'withvalidate'}) {
        if ((!$ofiles->{$entry->{'name'}} || $ofiles->{$entry->{'name'}} ne $entry->{'md5'}) &&
            (!$ofiles_expanded->{$entry->{'name'}} || $ofiles_expanded->{$entry->{'name'}} ne $entry->{'md5'})) {
          $entry->{'hash'} = 'missing';
          push @missing, $entry;
        }
      }
      $files->{$entry->{'name'}} = $entry->{'md5'};
    }
  }
  if (@missing) {
    my $res = {'name' => $packid, 'error' => 'missing', 'entry' => \@missing};
    return ($res, $BSXML::dir);
  }
  $files = BSSrcServer::Link::keeplink($cgi, $projid, $packid, $files) if $cgi->{'keeplink'};
  if (-e "$projectsdir/$projid.pkg/$packid.upload-MD5SUMS") {
    # autocommit old update revision so that it doesn't get lost
    my $uploadrev = {'project' => $projid, 'package' => $packid, 'srcmd5' => 'upload'};
    my $uploadfiles = BSRevision::lsrev($uploadrev);
    addrev({ %$cgi, 'comment' => 'autocommit update revision'}, $projid, $packid, $uploadfiles);
  }
  my $rev = addrev_runservice($cgi, $projid, $packid, $files);
  BSSrcServer::Service::runservice($cgi, $rev, $files) unless $cgi->{'noservice'};
  $cgi->{'rev'} = $rev->{'rev'};
  return getfilelist($cgi, $projid, $packid);
}

sub sourcecommitobsscm {
  my ($cgi, $projid, $packid) = @_;
  my $rev;
  if ($packid eq '_project') {
    die("Cannot use a srcmd5 for obsscm projects commits\n") if $cgi->{'srcmd5'};
    my $proj = BSRevision::readproj_local($projid);
    die("Project $projid is not controlled by obs-scm\n") unless $proj->{'scmsync'};
    die("Project $projid is a remote project\n") if $proj->{'remoteurl'};
    mkdir_p($uploaddir);
    my $uploaded = BSServer::read_file("$uploaddir/$$");
    die("upload failed\n") unless $uploaded;
    my $cpiofd;
    open($cpiofd, '<', "$uploaddir/$$") || die("$uploaddir/$$: $!\n");
    $rev = BSRevision::getrev_local($projid, $packid, '0');
    $rev->{'cpiofd'} = $cpiofd;
  } else {
    die("Need a srcmd5 for obsscm package commits\n") unless $cgi->{'srcmd5'};
    my $pack = BSRevision::readpack_local($projid, $packid);
    die("Package $packid is not controlled by obs-scm\n") unless $pack->{'scmsync'};
    $rev = BSRevision::getrev_local($projid, $packid, $cgi->{'srcmd5'});
  }
  die("No such revision $cgi->{'srcmd5'}\n") unless $rev;
  my $linkinfo = {};
  my $files = BSRevision::lsrev($rev, $linkinfo);
  die("Weird service run result\n") if %$linkinfo;
  $rev->{'user'} = $cgi->{'user'} if $cgi->{'user'};
  $rev->{'comment'} = $cgi->{'comment'} if $cgi->{'comment'};
  $rev->{'run'} = $cgi->{'run'};
  $rev->{'_service_info'} = $cgi->{'info'} if $cgi->{'info'};
  my $newrev = BSSrcServer::Service::commitobsscm($projid, $packid, $cgi->{'servicemark'}, $rev, $files) || {};
  delete $newrev->{'project'};
  delete $newrev->{'package'};
  return ($newrev, $BSXML::revision);
}

# admin only, move entire project
sub moveproject {
  my ($cgi, $projid) = @_;
  my $oprojid = $cgi->{'oproject'};
  return $BSStdServer::return_ok if $oprojid eq $projid;

  my $oproj = BSRevision::readproj_local($oprojid);

  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my $oreposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($oprojid) : $BSConfig::reposerver;
  if ($reposerver ne $oreposerver) {
    die("cannot copy binaries between different reposiory servers yet\n");
  }
  if (-e "$projectsdir/$projid.pkg" || -e "$projectsdir/$projid.conf" || -e "$projectsdir/$projid.xml") {
    die("target project already exists\n");
  }

  rename("$projectsdir/$oprojid.xml", "$projectsdir/$projid.xml");
  rename("$projectsdir/$oprojid.pkg", "$projectsdir/$projid.pkg") if -e "$projectsdir/$oprojid.pkg";
  rename("$projectsdir/$oprojid.conf", "$projectsdir/$projid.conf") if -e "$projectsdir/$oprojid.conf";
  if ($BSConfig::nosharedtrees && -e "$treesdir/$oprojid") {
    # we can't rename the tress as we still want to allow access of the old files via the srcmd5
    mkdir_p($uploaddir);
    for my $packid (sort(ls("$treesdir/$oprojid"))) {
      mkdir_p("$treesdir/$projid/$packid");
      for my $tree (sort(ls("$treesdir/$oprojid/$packid"))) {
	next if -e "$treesdir/$projid/$packid/$tree";
	BSUtil::cp("$treesdir/$oprojid/$packid/$tree", "$uploaddir/$$", "$treesdir/$projid/$packid/$tree");
      }
    }
  }

  # move entries in linkinfo database
  BSRevision::movelinkinfos($projid, $oprojid, BSSrcServer::LinkinfoDB::getlinkpackages($oprojid));
  BSSrcServer::ScmsyncDB::movescmsync($projid, $oprojid);

  # move in the backend as well
  my @args;
  push @args, "cmd=move";
  push @args, "oproject=$oprojid";
  my $param = {
    'uri' => "$reposerver/build/$projid",
    'request' => 'POST',
  };
  eval {
    # ignore failures for now
    BSWatcher::rpc($param, undef, @args);
  };
  warn($@) if $@;

  # check all packages in project
  notify_repservers('package', $projid);
  notify_repservers('package', $oprojid);
  return $BSStdServer::return_ok;
}

# copy sources of entire project, project exists ensured by api.
sub copyproject {
  my ($cgi, $projid) = @_;
  my $oprojid = $cgi->{'oproject'};
  return $BSStdServer::return_ok if $oprojid eq $projid;

  die("copyproject can not have both makeolder and makeoriginolder\n") if $cgi->{'makeolder'} && $cgi->{'makeoriginolder'};
  my $proj = BSRevision::readproj_local($projid);
  my $oproj = BSRevision::readproj_local($oprojid);

  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my $oreposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($oprojid) : $BSConfig::reposerver;
  if ($cgi->{'withbinaries'} && $reposerver ne $oreposerver) {
    die("cannot copy binaries between different repository servers yet\n");
  }

  my $user = defined($cgi->{'user'}) && $cgi->{'user'} ne '' ? $cgi->{'user'} : 'unknown';
  my $comment = defined($cgi->{'comment'}) ? $cgi->{'comment'} : '';
  $user = str2utf8xml($user);
  $comment = str2utf8xml($comment);

  # copy _project data
  if (-e "$projectsdir/$oprojid.pkg/_project.rev" || -e "$projectsdir/$oprojid.conf") {
    my $lastorev = getrev($oprojid, '_project');
    my $files = BSRevision::lsrev($lastorev);
    BSSrcrep::copyfiles($projid, '_project', $oprojid, '_project', $files);
    addrev($cgi, $projid, '_project', $files);
  }

  # signal start of project copy
  notify_repservers('suspendproject', $projid, undef, 'copyproject in progress');

  # local packages only
  my @pkgs = BSRevision::lspackages_local($oprojid);
  delete $cgi->{'servicemark'};		# just in case...
  for my $packid (@pkgs) {
    if (! -e "$projectsdir/$projid.pkg/$packid.xml") {
      # new package, create. hopefully the API can deal with this
      my $opack = BSRevision::readpack_local($oprojid, $packid);
      my $pack = {
	'project' => $projid,
	'name' => $packid,
      };
      # everything except person, group, devel and lock
      for (keys %$opack) {
        next if $_ eq 'project' || $_ eq 'name';
        next if $_ eq 'person' || $_ eq 'group' || $_ eq 'devel' || $_ eq 'lock';
        $pack->{$_} = $opack->{$_} if defined $opack->{$_};
      }
      mkdir_p($uploaddir);
      writexml("$uploaddir/copyproject$$", undef, $pack, $BSXML::pack);
      BSRevision::addrev_meta_replace($cgi, $projid, $packid, [ "$uploaddir/copyproject$$", "$projectsdir/$projid.pkg/$packid.xml", '_meta' ]);
      # need to do this now because the binary copy will fail otherwise
      notify_repservers('package', $projid, $packid) if $cgi->{'withbinaries'};
    }
    if ($cgi->{'makeolder'} || $cgi->{'makeoriginolder'} || -s "$projectsdir/$oprojid.pkg/$packid.rev") {
      my $lastorev;
      if ($cgi->{'withhistory'}) {
	# FIXME: races ahead
	# history copying is a bit tricky, as it renumbers the revisions
	my @allrevs = BSFileDB::fdb_getall("$projectsdir/$oprojid.pkg/$packid.rev", $srcrevlay);
	if (-e "$projectsdir/$projid.pkg/$packid.rev") {
	  my $lastrev = BSFileDB::fdb_getlast("$projectsdir/$projid.pkg/$packid.rev", $srcrevlay);
	  if ($lastrev && $lastrev->{'rev'}) {
	    for my $rev (@allrevs) {
	      $rev->{'rev'} += $lastrev->{'rev'};
	    }
	  }
	}
	# make trees available in new project
	for my $rev (@allrevs) {
	  BSSrcrep::copytree($projid, $packid, $oprojid, $packid, $rev->{'srcmd5'});
	}
	BSFileDB::fdb_add_multiple("$projectsdir/$projid.pkg/$packid.rev", $srcrevlay, @allrevs);
	$lastorev = $allrevs[-1];
      } else {
	$lastorev = BSFileDB::fdb_getlast("$projectsdir/$oprojid.pkg/$packid.rev", $srcrevlay);
      }
      if (!$lastorev || !$lastorev->{'rev'}) {
	next unless $cgi->{'makeolder'} || $cgi->{'makeoriginolder'};
	# fake empty commit
	$lastorev = { 'version' => 'unknown', 'rev' => 0, 'vrev' => 0, 'srcmd5' => $BSSrcrep::emptysrcmd5 };
      }
      # always do one new commit, we don't use addrev to have full control over vrev
      my $linkinfo = {};
      my $frev = { %$lastorev, 'project' => $oprojid, 'package' => $packid };
      my $files = BSRevision::lsrev($frev, $linkinfo);

      my $servicemark;
      if ($linkinfo->{'xservicemd5'}) {
        if ($cgi->{'noservice'}) {
	  my $slinkinfo = {};
	  eval {
	    $files = BSSrcServer::Service::handleservice($frev, $files, $linkinfo->{'xservicemd5'}, $slinkinfo);
	  };
	  if ($@) {	
	    warn($@);	# hmm, could not expand service
	    $servicemark = BSSrcServer::Service::genservicemark($projid, $packid, $files, undef, 1);
	  } else {
            BSSrcrep::copyfiles($projid, $packid, $oprojid, $packid, $files);
	    my $lxservicemd5 = $slinkinfo->{'lxservicemd5'};
	    BSSrcrep::copytree($projid, $packid, $oprojid, $packid, $lxservicemd5) if $lxservicemd5;
	    ($servicemark, $files) = BSSrcServer::Service::servicemark_noservice($cgi, $projid, $packid, $files, undef, $linkinfo->{'xservicemd5'}, $lxservicemd5);
	  }
	} else {
	  $servicemark = BSSrcServer::Service::genservicemark($projid, $packid, $files, undef, 1);
	}
      }
      BSSrcrep::copyfiles($projid, $packid, $oprojid, $packid, $files);
      $files->{'/SERVICE'} = $servicemark if $servicemark;
      my $srcmd5 = BSSrcrep::addmeta($projid, $packid, $files);
      delete $files->{'/SERVICE'};
      my $newrev = { %$lastorev };
      $newrev->{'srcmd5'} = $srcmd5;
      $newrev->{'user'} = $user;
      $newrev->{'comment'} = $comment;
      $newrev->{'requestid'} = $cgi->{'requestid'};
      $newrev->{'time'} = time();
      if ($cgi->{'makeolder'}) {
	$newrev->{'vrev'} =~ s/(\d+)$/($1+1).".1"/e;
      } elsif ($cgi->{'makeoriginolder'} && !$files->{'_link'}) {
	$newrev->{'vrev'} =~ s/(\d+)$/$1+2/e;
      } else {
	$newrev->{'vrev'} =~ s/(\d+)$/$1+1/e;
      }
      delete $newrev->{'rev'};
      $newrev = BSRevision::addrev_local({'vrev' => 1}, $projid, $packid, $newrev, $files);
      if ($cgi->{'makeolder'} || ($cgi->{'makeoriginolder'} && !$files->{'_link'})) {
	$lastorev->{'user'} = $user;
	$lastorev->{'comment'} = $comment;
	$lastorev->{'requestid'} = $cgi->{'requestid'};
	$lastorev->{'time'} = time();
	$lastorev->{'user'} = $user;
	if ($cgi->{'makeoriginolder'}) {
	  $lastorev->{'vrev'} =~ s/(\d+)$/($1+1).".1"/e;
	} else {
	  $lastorev->{'vrev'} =~ s/(\d+)$/$1+2/e;
	}
	delete $lastorev->{'rev'};
	$lastorev = BSFileDB::fdb_add_i("$projectsdir/$oprojid.pkg/$packid.rev", $srcrevlay, $lastorev);
      }
      if ($packid ne '_product') {
	my $omb = BSSrcServer::Multibuild::getmultibuild($oprojid, $packid);
	my $mb = BSSrcServer::Multibuild::getmultibuild($projid, $packid);
	if ($mb || $omb) {
          BSSrcServer::Multibuild::setmultibuild($projid, $packid, $omb);
          notify_repservers('package', $projid, $packid) if $cgi->{'withbinaries'};
	}
      }
    }
    # XXX: does this make any sense?
    if ($cgi->{'withbinaries'}) {
      for my $repo (@{$proj->{'repository'} || []}) {
	my $orepo = (grep {$_->{'name'} eq $repo->{'name'}} @{$oproj->{'repository'} || []})[0];
	next unless $orepo;
	for my $arch (@{$repo->{'arch'} || []}) {
	  next unless grep {$_ eq $arch} @{$orepo->{'arch'} || []};

	  # same source and target repo/arch in both projects exists
	  for my $mpackid (BSSrcServer::Multibuild::addmultibuildpackages($projid, undef, $packid)) {
	    my @args;
	    push @args, "cmd=copy";
	    push @args, "oproject=$oprojid";
	    push @args, "opackage=$mpackid"; # same package name
	    push @args, "orepository=$repo->{'name'}"; # same repo name
	    push @args, 'resign=1' if $cgi->{'resign'};
	    my $param = {
	      'uri' => "$reposerver/build/$projid/$repo->{'name'}/$arch/$mpackid",
	      'request' => 'POST',
	    };
	    eval {
	      # ignore failures for now
	      BSWatcher::rpc($param, undef, @args);
	    };
	    warn($@) if $@;
	  }
	}
      }
    }
  }
  # check all packages in project
  notify_repservers('package', $projid);	# also resumes the project
  if ($cgi->{'makeolder'} || $cgi->{'makeoriginolder'}) {
    notify_repservers('package', $oprojid);
  }
  return $BSStdServer::return_ok;
}

sub freezeprojectlink {
  my ($cgi, $projid) = @_;
  my %origins;
  my $proj = BSRevision::readproj_local($projid);
  my @frozen;
  for my $lprojid (map {$_->{'project'}} @{$proj->{'link'} || []}) {
    my @frozenp;
    my @lpackids = findpackages($projid, undef, -1);
    for my $packid (sort(@lpackids)) {
      my $rev = getrev($projid, $packid);
      lsrev_expanded($rev);
      push @frozenp, { name => $packid, srcmd5 => $rev->{'srcmd5'}, 'vrev' => $rev->{'vrev'} };
    }
    push @frozen, { 'project' => $lprojid, 'package' => \@frozenp };
  }
  my $frozenlinks = { 'frozenlink' => \@frozen };
  mkdir_p($uploaddir);
  writexml("$uploaddir/freezeproject.$$", undef, $frozenlinks, $BSXML::frozenlinks);
  $cgi->{'comment'} ||= 'freeze project';
  BSRevision::addrev_meta_replace($cgi, $projid, undef, [ "$uploaddir/freezeproject.$$", undef, '_frozenlinks' ]);
  return $BSStdServer::return_ok;
}

# we're going to auto-update a link. this means we must also
# auto-update the corresponding service result
# there must be an addrev with the same files and the servicemark after this!
sub update_link_in_service {
  my ($rev, $files, $xservicemd5, $isbranch) = @_;

  return undef unless defined $xservicemd5;
  return $xservicemd5 if $BSConfig::old_style_services;
  return $xservicemd5 unless $files->{'_link'};
  my $linkinfo = {};
  my $sfiles;
  eval { $sfiles = BSRevision::lsrev({%$rev, 'srcmd5' => $xservicemd5}, $linkinfo) };
  return $xservicemd5 unless $sfiles && $sfiles->{'_link'};
  return $xservicemd5 if $sfiles->{'_link'} && $sfiles->{'_link'} eq $files->{'_link'};	# nothing changed
  # okay, we need to generate a new service commit
  my $servicemark = BSSrcServer::Service::genservicemark($rev->{'project'}, $rev->{'package'}, $files, undef, 1);
  return undef unless $servicemark;
  # delete all non-service files unless it's a branch
  if (!$isbranch) {
    delete $sfiles->{$_} for grep {!/^_service[_:]/} keys %$sfiles;
  }
  # copy new link
  $sfiles->{'_link'} = $files->{'_link'};
  # write back new service result, reuse old lxservicemd5
  BSSrcServer::Service::fake_service_run($rev->{'project'}, $rev->{'package'}, $files, $sfiles, $servicemark, $linkinfo->{'lxservicemd5'});
  return $servicemark;
}


sub sourcecopy_update_baserev {
  my ($cgi, $projid, $packid, $oprojid, $opackid, $oldbaserev, $newbaserev, $vrevbump) = @_;
  my $latestorev = getrev($oprojid, $opackid);
  my $latestlinkinfo = {};
  my $latestfiles = BSRevision::lsrev($latestorev, $latestlinkinfo);
  return unless $latestfiles->{'_link'};
  my $latestl = BSRevision::revreadxml($latestorev, '_link', $latestfiles->{'_link'}, $BSXML::link, 1);
  my $latestisbranch = grep {(keys %$_)[0] eq 'branch'} @{$latestl->{'patches'}->{''} || []};
  return unless $latestisbranch && $latestl->{'baserev'} eq $oldbaserev;
  $latestl->{'baserev'} = $newbaserev;
  $latestl->{'patches'}->{''} = [ { 'branch' => undef} ]; # work around xml problem
  if ($latestl->{'missingok'} &&
    (defined($latestl->{'project'}) ? $latestl->{'project'} : $oprojid) eq $projid &&
    (defined($latestl->{'package'}) ? $latestl->{'package'} : $opackid) eq $packid) {
    eval {
      BSSrcServer::Access::checksourceaccess($projid, $packid);
      delete $latestl->{'missingok'};
    };
  }
  mkdir_p($uploaddir);
  writexml("$uploaddir/$$", undef, $latestl, $BSXML::link);
  $latestfiles->{'_link'} = BSSrcrep::addfile($oprojid, $opackid, "$uploaddir/$$", '_link');
  if ($vrevbump) {
    $cgi->{'vrev'} = $latestorev->{'vrev'};
    $cgi->{'vrev'} =~ s/(\d+)$/$1 + $vrevbump/e;
  }
  my $servicemark = update_link_in_service($latestorev, $latestfiles, $latestlinkinfo->{'xservicemd5'}, 1);
  addrev({ %$cgi, 'user' => 'buildservice-autocommit', 'comment' => 'baserev update by copy to link target', 'servicemark' => $servicemark }, $oprojid, $opackid, $latestfiles);
}

sub sourcecopy_update_link {
  my ($cgi, $projid, $packid, $oprojid, $opackid, $oldsrcmd5, $vrevbump) = @_;
  my $latestorev = getrev($oprojid, $opackid);
  return if $latestorev->{'srcmd5'} ne $oldsrcmd5;
  # simplify link
  my $latestlinkinfo = {};
  my $latestfiles = BSRevision::lsrev($latestorev, $latestlinkinfo);
  return unless $latestfiles->{'_link'};
  my $latestl = BSRevision::revreadxml($latestorev, '_link', $latestfiles->{'_link'}, $BSXML::link, 1);
  my $latestisbranch = grep {(keys %$_)[0] eq 'branch'} @{$latestl->{'patches'}->{''} || []};
  return if $latestisbranch;
  delete $latestl->{'patches'};
  delete $latestl->{'baserev'};
  mkdir_p($uploaddir);
  writexml("$uploaddir/$$", undef, $latestl, $BSXML::link);
  my $ofiles = {};
  $ofiles->{'_link'} = BSSrcrep::addfile($oprojid, $opackid, "$uploaddir/$$", '_link');
  if ($vrevbump) {
    $cgi->{'vrev'} = $latestorev->{'vrev'};
    $cgi->{'vrev'} =~ s/(\d+)$/$1 + $vrevbump/e;
  }
  my $servicemark = update_link_in_service($latestorev, $ofiles, $latestlinkinfo->{'xservicemd5'}, 0);
  addrev({ %$cgi, 'user' => 'buildservice-autocommit', 'comment' => 'auto commit by copy to link target', 'servicemark' => $servicemark }, $oprojid, $opackid, $ofiles);
}

sub sourcecopy {
  my ($cgi, $projid, $packid) = @_;
  die("illegal rev parameter\n") if $cgi->{'rev'} && $cgi->{'rev'} ne 'upload' && $cgi->{'rev'} ne 'repository';
  my $oprojid = exists($cgi->{'oproject'}) ? $cgi->{'oproject'} : $projid;
  my $opackid = exists($cgi->{'opackage'}) ? $cgi->{'opackage'} : $packid;
  die("makeoriginolder only makes sense with withvrev\n") if $cgi->{'makeoriginolder'} && !$cgi->{'withvrev'};
  die("vrevbump only makes sense with withvrev\n") if $cgi->{'vrevbump'} && !$cgi->{'withvrev'};
  my $orev = $cgi->{'orev'};
  $orev = getrev($oprojid, $opackid, defined($orev) ? $orev : 'latest');
  if ($cgi->{'instantiate'}) {
    my $proj = BSRevision::readproj_local($projid, 1);
    if ($proj && $proj->{'link'}) {
      my $irev = BSSrcServer::Projlink::getrev_projlink($projid, $proj, $packid, undef, undef, 1);
      if ($irev) {
	lsrev_expanded($irev);
	$orev->{'vrev'} = $irev->{'vrev'};	# hack: overwrite vrev with instantiated vrev
	$cgi->{'withvrev'} = 1;
      }
    }
  }
  die("origin must not be virtual for makeoriginolder\n") if $cgi->{'makeoriginolder'} && $orev->{'originproject'};
  $orev->{'linkrev'} = $cgi->{'olinkrev'} if $cgi->{'olinkrev'};
  my $orev_srcmd5 = $orev->{'srcmd5'};  # so that we can restore it later
  my $linkinfo = {};
  my $files = $cgi->{'noservice'} || $cgi->{'expand'} ? lsrev_service($orev, $linkinfo) : BSRevision::lsrev($orev, $linkinfo);
  my $lxservicemd5 = $cgi->{'noservice'} ? $linkinfo->{'lxservicemd5'} : undef;
  die("need a revision to copy\n") if !$cgi->{'rev'} && !$cgi->{'orev'} && $oprojid eq $projid && $opackid eq $packid && !$cgi->{'instantiate'} && !($files->{'_link'} && $cgi->{'expand'});

  die("makeoriginolder currently does not work on links\n") if $cgi->{'makeoriginolder'} && $files->{'_link'};

  my $autosimplifylink;
  my $autosimplifylink_lrev;
  my $freezelink;
  my $freezelinkfiles;

  if ($files->{'_link'} && !$cgi->{'dontupdatesource'} && !$cgi->{'rev'}) {
    # fix me: do this in a more generic way
    my $olink = BSRevision::revreadxml($orev, '_link', $files->{'_link'}, $BSXML::link, 1);
    if ($olink) {
      my $lprojid = $oprojid;
      my $lpackid = $opackid;
      my $lrev = $olink->{'rev'};
      $lprojid = $olink->{'project'} if exists $olink->{'project'};
      $lpackid = $olink->{'package'} if exists $olink->{'package'};
      if ($cgi->{'freezelink'}) {
	# we're going to freeze the link in the source
	die("400 freezelink needs expand or noservice\n") unless $cgi->{'noservice'} || $cgi->{'expand'};
	$lrev = getrev($lprojid, $lpackid, $lrev, undef, $olink->{'missingok'});
	my %lrev = %$lrev;
	lsrev_expanded(\%lrev);
	die("400 freezelink refusing to change rev from $olink->{'rev'} to $lrev{'srcmd5'}\n") if $olink->{'rev'} && $lrev{'srcmd5'} ne $olink->{'rev'};
	if (!$olink->{'rev'} || $lrev{'srcmd5'} ne $olink->{'rev'}) {
	  # halt, freeze!
	  $olink->{'rev'} = $lrev{'srcmd5'};
	  $olink->{'vrev'} = $lrev{'vrev'} if defined $lrev{'vrev'};
	  if ($lprojid eq $projid && $lpackid eq $packid) {
	    eval {
	      BSSrcServer::Access::checksourceaccess($projid, $packid);
	      delete $olink->{'missingok'};
	    };
	  }
	  $freezelink = $olink;
	}
	$freezelinkfiles = { %$files };
      } elsif ($lprojid eq $projid && $lpackid eq $packid) {
	# copy destination is target of link
	# we're integrating this link
	$lrev = getrev($lprojid, $lpackid, $lrev);
	$autosimplifylink_lrev = { %$lrev };
	my $lfiles = $cgi->{'noservice'} && !$cgi->{'expand'} ? lsrev_service({ %$lrev }) : BSRevision::lsrev($lrev);
	if ($lfiles->{'_link'} && !$cgi->{'expand'}) {
	  # link to a link, join
	  $files = BSSrcServer::Link::integratelink($lfiles, $lprojid, $lpackid, $lrev, $files, $oprojid, $opackid, $olink, $orev);
	} else {
	  # auto expand
	  $cgi->{'expand'} = 1;
	}
	$autosimplifylink = $olink;
      }
    }
  }

  if (!$files->{'_link'} && !$cgi->{'dontupdatesource'} && !$cgi->{'rev'} && $linkinfo->{'lsrcmd5'} && !$cgi->{'freezelink'}) {
    eval {
      my $uxrev = getrev($oprojid, $opackid, $linkinfo->{'lsrcmd5'});
      my $uxfiles = BSRevision::lsrev($uxrev);
      my $olink = BSRevision::revreadxml($uxrev, '_link', $uxfiles->{'_link'}, $BSXML::link, 1);
      if ($olink) {
	my $lprojid = $oprojid;
	my $lpackid = $opackid;
	my $lrev = $olink->{'rev'};
	$lprojid = $olink->{'project'} if exists $olink->{'project'};
	$lpackid = $olink->{'package'} if exists $olink->{'package'};
	if ($lprojid eq $projid && $lpackid eq $packid) {
	  $lrev = getrev($lprojid, $lpackid, $lrev);
	  $autosimplifylink_lrev = { %$lrev };
	  $autosimplifylink = $olink;
	  $orev_srcmd5 = $uxrev->{'srcmd5'};
	}
      }
    };
    warn("sourcecopy unexpand: $@") if $@;
  }

  die("400 freezelink: origin provides no link\n") if $cgi->{'freezelink'} && !$freezelinkfiles;

  my $oldvrev = $orev->{'vrev'};
  if ($files->{'_link'} && $cgi->{'expand'}) {
    my %olrev = %$orev;		# copy so that orev still points to unexpanded sources
    $files = BSSrcServer::Link::handlelinks(\%olrev, $files);
    die("broken link in $oprojid/$opackid: $files\n") unless ref $files;
    $oldvrev = $olrev{'vrev'};
  }

  BSSrcrep::copyfiles($projid, $packid, $oprojid, $opackid, $files);
  BSSrcrep::copytree($projid, $packid, $oprojid, $opackid, $lxservicemd5) if $lxservicemd5;

  # copy multibuild data
  if ($cgi->{'expand'} && $cgi->{'noservice'} && $packid ne '_product') {
    my $omb = BSSrcServer::Multibuild::getmultibuild($oprojid, $opackid);
    BSSrcServer::Multibuild::setmultibuild($projid, $packid, $omb);
  }

  if ($cgi->{'withvrev'} && !$cgi->{'vrev'} && defined($oldvrev)) {
    $cgi->{'vrev'} = $oldvrev;
    my $vrevbump = $cgi->{'vrevbump'} || 1;
    $vrevbump = 2 if $cgi->{'makeoriginolder'} && $vrevbump < 2;
    # bump vrev so that new builds will have a bigger release number
    # (just like in copyproject)
    $cgi->{'vrev'} =~ s/(\d+)$/$1+$vrevbump/e;
  }
  $files = BSSrcServer::Link::keeplink($cgi, $projid, $packid, $files) if $cgi->{'keeplink'};
  $cgi->{'lxservicemd5'} = $lxservicemd5 if $lxservicemd5;
  my $rev = addrev($cgi, $projid, $packid, $files, $cgi->{'rev'});
  delete $cgi->{'vrev'};
  delete $cgi->{'lxservicemd5'};

  if ($cgi->{'makeoriginolder'}) {
    # add dummy commit
    my $lastline = BSFileDB::fdb_getlast("$projectsdir/$oprojid.pkg/$opackid.rev", $srcrevlay);
    die("makeoriginolder: $oprojid/$opackid does not exists?\n") unless defined $lastline;
    delete $lastline->{'requestid'};
    delete $lastline->{'rev'};
    $lastline->{'user'} = 'buildservice-autocommit';
    $lastline->{'comment'} = "makeoriginolder vrev update for $projid/$packid";
    $lastline->{'requestid'} = $cgi->{'requestid'} if $cgi->{'requestid'};
    $lastline->{'vrev'} =~ s/(\d+)$/($1+1).".1"/e;
    BSFileDB::fdb_add_i("$projectsdir/$oprojid.pkg/$opackid.rev", $srcrevlay, $lastline);
    notify_repservers('package', $oprojid, $opackid);
  }

  if ($freezelink) {
    mkdir_p($uploaddir);
    writexml("$uploaddir/$$", undef, $freezelink, $BSXML::link);
    $freezelinkfiles->{'_link'} = BSSrcrep::addfile($oprojid, $opackid, "$uploaddir/$$", '_link');
    my $isbranch = grep {(keys %$_)[0] eq 'branch'} @{$freezelink->{'patches'}->{''} || []};
    my $servicemark = update_link_in_service($orev, $freezelinkfiles, $linkinfo->{'xservicemd5'}, $isbranch);
    addrev({ %$cgi, 'user' => 'buildservice-autocommit', 'comment' => 'freeze link', 'servicemark' => $servicemark }, $oprojid, $opackid, $freezelinkfiles);
  } elsif ($autosimplifylink && !defined($autosimplifylink->{'rev'})) {
    $orev->{'srcmd5'} = $orev_srcmd5;	# back to unexpanded

    # make sure that vrev doesn't decrease when copying to the
    # link target
    my $vrevbump = 0;
    if ($rev && $autosimplifylink_lrev && $rev->{'version'} ne $autosimplifylink_lrev->{'version'}) {
      # version change, check if vrev went down
      my $vrev1 = $rev->{'vrev'} || '0';
      my $vrev2 = $autosimplifylink_lrev->{'vrev'} || '0';
      $vrev1 =~ s/.*?(\d+)$/$1/;
      $vrev2 =~ s/.*?(\d+)$/$1/;
      $vrevbump = $vrev2 > $vrev1 ? $vrev2 - $vrev1 : 0;
    }

    my $isbranch = grep {(keys %$_)[0] eq 'branch'} @{$autosimplifylink->{'patches'}->{''} || []};
    if ($isbranch) {
      # update base rev so that there are no changes
      # FIXME: this is a gross hack...
      # we should not need to update the baserev, instead we should change
      # the way branches get applied
      my $ofiles = BSRevision::lsrev($orev);
      delete $ofiles->{'_link'};
      BSSrcrep::copyfiles($projid, $packid, $oprojid, $opackid, $ofiles);
      my $newbaserev = BSSrcrep::addmeta($projid, $packid, $ofiles);
      if ($autosimplifylink->{'baserev'} ne $newbaserev) {
	eval { sourcecopy_update_baserev($cgi, $projid, $packid, $oprojid, $opackid, $autosimplifylink->{'baserev'}, $newbaserev, $vrevbump) };
        warn("sourcecopy update baserev: $@") if $@;
      }
    } else {
      eval { sourcecopy_update_link($cgi, $projid, $packid, $oprojid, $opackid, $orev->{'srcmd5'}, $vrevbump) };
      warn("sourcecopy update link: $@") if $@;
    }
    delete $cgi->{'vrev'} if $vrevbump;
  }

  BSSrcServer::Service::runservice($cgi, $rev, $files) unless $cgi->{'noservice'};

  delete $rev->{'project'};
  delete $rev->{'package'};
  return ($rev, $BSXML::revision_acceptinfo);
}

sub sourcebranch {
  my ($cgi, $projid, $packid) = @_;

  my $usebranch = $cgi->{'simplelink'} ? 0 : 1;
  my $oprojid = exists($cgi->{'oproject'}) ? $cgi->{'oproject'} : $projid;
  my $opackid = exists($cgi->{'opackage'}) ? $cgi->{'opackage'} : $packid;
  my $orev = $cgi->{'orev'};
  die("cannot branch myself\n") if $oprojid eq $projid && $opackid eq $packid;
  $orev = getrev($oprojid, $opackid, defined($orev) ? $orev : 'latest', undef, $cgi->{'missingok'});
  $opackid = $orev->{'package'} if $opackid =~ /(?<!^_product)(?<!^_patchinfo):./;
  $orev->{'linkrev'} = $cgi->{'olinkrev'} if $cgi->{'olinkrev'};
  my $linkinfo = {};
  my $files = lsrev_expanded($orev, $linkinfo);	# modifies srcmd5, thus also needed for keepcontent case
  if ($cgi->{'keepcontent'}) {
    die("keepcontent is only supported for branches\n") unless $usebranch;
    my $nrev = getrev($projid, $packid, 'latest');
    $files = lsrev_expanded($nrev, $linkinfo);
  }
  my $lxservicemd5 = $cgi->{'noservice'} ? $linkinfo->{'lxservicemd5'} : undef;
  my $l = {};
  $l->{'project'} = $oprojid if $oprojid ne $projid;
  $l->{'package'} = $opackid if $opackid ne $packid;
  # a missing package entry is bad if the project has sourceaccess
  # disabled, so check if that's the case
  eval {
    BSSrcServer::Access::checksourceaccess($oprojid, $opackid) if $opackid eq $packid && $oprojid ne $projid;
  };
  $l->{'package'} = $opackid if $@;
  $l->{'missingok'} = 'true' if defined $cgi->{'missingok'} && !exists($orev->{'rev'}) && $orev->{'srcmd5'} eq $BSSrcrep::emptysrcmd5;
  $l->{'rev'} = $cgi->{'orev'} if defined $cgi->{'orev'};
  $l->{'baserev'} = $orev->{'srcmd5'};
  if ($cgi->{'extendvrev'}) {
    $l->{'vrev'} = $orev->{'vrev'};
    $l->{'vrev'} ||= '0';
    die("extendvrev error for $l->{'vrev'}\n") unless $l->{'vrev'} =~ s/^(\d+).*?$/($1+1).'.1'/e;
  }
  my $lfiles = {};
  if ($usebranch) {
    $l->{'patches'}->{''} = [ { 'branch' => undef} ];
    BSSrcrep::copyfiles($projid, $packid, $oprojid, $opackid, $files) unless $cgi->{'keepcontent'};
    $lfiles->{$_} = $files->{$_} for keys %$files;
  }
  BSSrcrep::copytree($projid, $packid, $oprojid, $opackid, $lxservicemd5) if $lxservicemd5;
  $cgi->{'lxservicemd5'} = $lxservicemd5 if $lxservicemd5;
  mkdir_p($uploaddir);
  writexml("$uploaddir/$$", undef, $l, $BSXML::link);
  $lfiles->{'_link'} = BSSrcrep::addfile($projid, $packid, "$uploaddir/$$", '_link');
  my $rev = addrev_runservice($cgi, $projid, $packid, $lfiles);
  delete $rev->{'project'};
  delete $rev->{'package'};
  return ($rev, $BSXML::revision_acceptinfo);
}

sub linktobranch {
  my ($cgi, $projid, $packid) = @_;
  my $rev = getrev($projid, $packid);
  $rev->{'linkrev'} = $cgi->{'linkrev'} if $cgi->{'linkrev'};
  my $files = BSRevision::lsrev($rev);
  die("package is not a link\n") unless $files->{'_link'};
  my $l = BSRevision::revreadxml($rev, '_link', $files->{'_link'}, $BSXML::link);
  die("package is already a branch\n") if $l->{'patches'} && grep {(keys %$_)[0] eq 'branch'} @{$l->{'patches'}->{''} || []};
  my $linkinfo = {};
  $files = lsrev_expanded($rev, $linkinfo);
  $cgi->{'lxservicemd5'} = $linkinfo->{'lxservicemd5'} if $linkinfo->{'lxservicemd5'};
  $l->{'baserev'} = $linkinfo->{'srcmd5'};
  $l->{'patches'}->{''} = [ { 'branch' => undef} ];
  mkdir_p($uploaddir);
  writexml("$uploaddir/$$", undef, $l, $BSXML::link);
  $files->{'_link'} = BSSrcrep::addfile($projid, $packid, "$uploaddir/$$", '_link');
  $cgi->{'comment'} ||= 'converted link to branch';
  $rev = addrev_runservice($cgi, $projid, $packid, $files);
  delete $rev->{'project'};
  delete $rev->{'package'};
  return ($rev, $BSXML::revision);
}

sub sourcecollectbuildenv {
  my ($cgi, $projid, $packid) = @_;
  my $oprojid = $cgi->{'oproject'} || $projid;
  my $opackid = $cgi->{'opackage'} || $packid;
  die("cannot collect from myself\n") if $oprojid eq $projid && $opackid eq $packid;
  my $proj = checkprojrepoarch($projid);
  my $oproj = checkprojrepoarch($oprojid);
  my %orepoids;
  for (@{$oproj->{'repository'} || []}) {
    $orepoids{"$oprojid/$_->{'name'}"} = $_;
  }
  for (@{$oproj->{'repository'} || []}) {
    for my $rt (@{$_->{'releasetarget'} || []}) {
      $orepoids{"$rt->{'project'}/$rt->{'repository'}"} ||= $_;
    }
  }
  my %buildenvs;
  for my $repo (@{$proj->{'repository'} || []}) {
    next unless @{$repo->{'arch'} || []};
    my $repoid = $repo->{'name'};
    my @xpath = expandsearchpath($projid, $repoid);
    my $orepo;
    for my $xr (@xpath) {
      $orepo = $orepoids{$xr};
      last if $orepo;
    }
    if ($orepo) {
      my $orepoid = $orepo->{'name'};
      my %oarchs = map {$_ => 1} @{$orepo->{'arch'} || []};
      for my $arch (@{$repo->{'arch'}}) {
	my $be;
	if (!$oarchs{$arch}) {
	  # arch not included, use error buildenv
	  $be = { 'error', "$arch missing in $oprojid/$orepoid" };
	} else {
	  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($oprojid) : $BSConfig::reposerver;
	  eval {
	    $be = BSRPC::rpc("$reposerver/build/$oprojid/$orepoid/$arch/$opackid/_buildenv", $BSXML::buildinfo);
	  };
	  if ($@) {
	    die($@) if $@ !~ /^404/;
	    $be = { 'error', "_buildenv missing in $oprojid/$orepoid" };
	  }
	  $be ||= { 'error', "could not get _buildenv in $oprojid/$orepoid" };
	}
        $buildenvs{"_buildenv.$repoid.$arch"} = BSUtil::toxml($be, $BSXML::buildinfo);
      }
    }
  }
  die("could not get any buildenv, something is wrong\n") unless %buildenvs;
  # add master buildenv, in our case a "fallback error" buildenv
  my $be = { 'error', "no buildenv for this repo/arch" };
  $buildenvs{'_buildenv'} = BSUtil::toxml($be, $BSXML::buildinfo);
  # now add all the buildenvs to the last commit (unexpanded is enough for us)
  my $rev = getrev($projid, $packid);
  my $files = BSRevision::lsrev($rev);
  delete $files->{$_} for grep {/^_buildenv/} keys %$files;
  mkdir_p($uploaddir);
  for my $file (sort keys %buildenvs) {
    writestr("$uploaddir/_be$$", undef, $buildenvs{$file});
    $files->{$file} = BSSrcrep::addfile($projid, $packid, "$uploaddir/_be$$", $file);
  }
  $rev = addrev_runservice($cgi, $projid, $packid, $files);
  delete $rev->{'project'};
  delete $rev->{'package'};
  return ($rev, $BSXML::revision);
}

sub deleteuploadrev {
  my ($cgi, $projid, $packid) = @_;
  unlink("$projectsdir/$projid.pkg/$packid.upload-MD5SUMS");
  return $BSStdServer::return_ok;
}

sub unknowncmd {
  my ($cgi, $projid, $packid) = @_;
  die("unknown command \"$cgi->{'cmd'}\"\n");
}

sub delfile {
  my ($cgi, $projid, $packid, $filename) = @_;
  die("no filename\n") unless defined($filename) && $filename ne '';
  die("bad filename\n") if $filename =~ /\// || $filename =~ /^\./;
  if ($cgi->{'meta'}) {
    if ($filename ne '_attribute' && $filename ne '_frozenlinks') {
      die("unsupported meta operation\n");
    }
    my $rev = BSRevision::addrev_meta_replace($cgi, $projid, $packid, [ undef, undef, $filename ]);
    notify_repservers('package', $projid) if $filename eq '_frozenlinks';
    delete $rev->{'project'};
    delete $rev->{'package'};
    return ($rev, $BSXML::revision);
  }
  die("file '$filename' is read-only\n") if ($filename =~ /^_service:/) && !$cgi->{'force'};
  my $rev = getrev($projid, $packid, defined($cgi->{'rev'}) ? $cgi->{'rev'} : 'upload');
  my $files;
  if ($cgi->{'keeplink'}) {
    $files = lsrev_expanded($rev);
  } else {
    $files = BSRevision::lsrev($rev);
  }
  die("404 file '$filename' does not exist\n") unless $files->{$filename};
  delete $files->{$filename};
  $files = BSSrcServer::Link::keeplink($projid, $packid, $files) if $cgi->{'keeplink'};
  $rev = addrev_runservice($cgi, $projid, $packid, $files, $cgi->{'rev'});
  delete $rev->{'project'};
  delete $rev->{'package'};
  return ($rev, $BSXML::revision);
}

sub getrepositorylist {
  my ($cgi, $projid) = @_;
  my $proj = checkprojrepoarch($projid, undef, undef, 1);
  if ($proj->{'remoteurl'}) {
    return (BSRPC::rpc({'uri' => "$proj->{'remoteurl'}/build/$proj->{'remoteproject'}", 'proxy' => $proxy}, $BSXML::dir), $BSXML::dir);
  }
  my @res = map {{'name' => $_->{'name'}}} @{$proj->{'repository'} || []};
  return ({'entry' => \@res}, $BSXML::dir);
}

sub getrepository {
  my ($cgi, $projid, $repoid) = @_;
  my $proj = BSRevision::readproj_local($projid);
  my $repo = (grep {$_->{'name'} eq $repoid} @{$proj->{'repository'} || []})[0];
  die("404 $repoid: no such repository\n") unless $repo;
  return ($repo, $BSXML::repo);
}

sub getarchlist {
  my ($cgi, $projid, $repoid) = @_;
  my $proj = checkprojrepoarch($projid, $repoid, undef, 1);
  if ($proj->{'remoteurl'}) {
    return (BSRPC::rpc({'uri' => "$proj->{'remoteurl'}/build/$proj->{'remoteproject'}/$repoid", 'proxy' => $proxy}, $BSXML::dir), $BSXML::dir);
  }
  my @repo = grep {$_->{'name'} eq $repoid} @{$proj->{'repository'} || []};
  die("404 $repoid: no such repository\n") unless @repo;
  my @res = map {{'name' => $_}} @{$repo[0]->{'arch'} || []};
  return ({'entry' => \@res}, $BSXML::dir);
}

sub getresult {
  my ($cgi, $projid) = @_;

  my $proj = checkprojrepoarch($projid, undef, undef, 1);
  if ($proj->{'remoteurl'}) {
    die("oldstate not supported for remote projects\n") if $cgi->{'oldstate'};
    my @args = BSRPC::args($cgi, 'lastbuild', 'view', 'repository', 'arch', 'package', 'code', 'multibuild', 'locallink');
    return (BSRPC::rpc({'uri' => "$proj->{'remoteurl'}/build/$proj->{'remoteproject'}/_result", 'proxy' => $proxy}, $BSXML::resultlist, @args), $BSXML::resultlist);
  }
  if ($cgi->{'multibuild'} && $cgi->{'package'} && !$BSStdServer::isajax) {
    $cgi->{'package'} = [ BSSrcServer::Multibuild::addmultibuildpackages($projid, undef, @{$cgi->{'package'}}) ];
  }
  if ($cgi->{'locallink'} && $cgi->{'package'} && !$BSStdServer::isajax) {
    $cgi->{'package'} = [ BSSrcServer::LinkinfoDB::addlocallinks($projid, @{$cgi->{'package'}}) ];
  }
  if ($cgi->{'oldstate'} && !$BSStdServer::isajax) {
    my @args = BSRPC::args($cgi, 'oldstate', 'lastbuild', 'view', 'repository', 'arch', 'package', 'code');
    BSHandoff::handoff("/build/$projid/_result", undef, @args);
  }

  my %repoidfilter = map {$_ => 1} @{$cgi->{'repository'} || []};
  my %archfilter = map {$_ => 1} @{$cgi->{'arch'} || []};
  my %view = map {$_ => 1} @{$cgi->{'view'} || ['status']};
  $view{'status'} = 1 if $view{'versrel'};
  my %code = map {$_ => 1} @{$cgi->{'code'} || []};

  if ($cgi->{'repository'}) {
    my %knownrepoids = map {$_->{'name'} => 1} @{$proj->{'repository'} || []};
    for (@{$cgi->{'repository'}}) {
      die("404 unknown repository '$_'\n") if !$knownrepoids{$_};
    }
  }
  if ($cgi->{'package'}) {
    my %knownpackids = map {$_ => 1} findpackages($projid, $proj, 1);
    for (@{$cgi->{'package'}}) {
      die("404 unknown package '$_'\n") if !$knownpackids{$_};
    }
  }
  my @prpas;
  for my $repo (@{$proj->{'repository'} || []}) {
    next if %repoidfilter && !$repoidfilter{$repo->{'name'}};
    my @archs = @{$repo->{'arch'} || []};
    @archs = grep {$archfilter{$_}} @archs if %archfilter;
    push @prpas, map {"$projid/$repo->{'name'}/$_"} @archs;
  }

  BSWatcher::addfilewatcher("$projectsdir/$projid.xml") if $BSStdServer::isajax;

  if (!@prpas) {
    my $state = "00000000000000000000000000000000";
    return undef if $BSStdServer::isajax && $cgi->{'oldstate'} && $state eq $cgi->{'oldstate'};
    return ({'state' => $state}, $BSXML::resultlist);
  }

  my $ps = {};
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my @args;
  push @args, 'lastbuild' if $cgi->{'lastbuild'};
  push @args, "oldstate=$cgi->{'oldstate'}" if $cgi->{'oldstate'};
  push @args, map {"prpa=$_"} @prpas;
  push @args, map {"package=$_"} @{$cgi->{'package'} || []};
  push @args, map {"code=$_"} @{$cgi->{'code'} || []};
  push @args, 'withbinarylist' if $view{'binarylist'};
  push @args, 'withstats' if $view{'stats'};
  push @args, 'withinfo' if $view{'info'};
  push @args, 'withversrel' if $view{'versrel'};
  push @args, 'summary' if $view{'summary'} && !$view{'status'};
  eval {
    $ps = BSWatcher::rpc("$reposerver/_result", $BSXML::resultlist, @args);
  };
  if ($@) {
    print "warning: $reposerver: $@";
    $ps = {};
  }
  return if $BSStdServer::isajax && !defined($ps);
  if ($view{'summary'} && $view{'status'}) {
    my @order = ('succeeded', 'failed', 'unresolvable', 'broken', 'scheduled');
    my %order = map {$_ => 1} @order;
    for my $p (@{$ps->{'result'} || []}) {
      my %sum;
      for my $pp (@{$p->{'status'} || []}) {
        $sum{$pp->{'code'}}++ if $pp->{'code'};
      }
      my @sum = grep {exists $sum{$_}} @order;
      push @sum, grep {!$order{$_}} sort keys %sum;
      $p->{'summary'} = {'statuscount' => [ map {{'code' => $_, 'count' => $sum{$_}}} @sum ] };
    }
  }
  if (!$view{'status'}) {
    for my $p (@{$ps->{'result'} || []}) {
      delete $p->{'status'};
    }
  }
  return ($ps, $BSXML::resultlist);
}

sub getscmresult {
  my ($cgi) = @_;

  my $scmrepo = $cgi->{'scmrepository'};
  my $scmbranch = $cgi->{'scmbranch'};

  my $results = [];

  for my $ref (BSSrcServer::ScmsyncDB::getscmsyncpackages($scmrepo, $scmbranch)) {
    my $projid = @$ref[0];

    my $packids;
    $packids = [@$ref[1]] unless @$ref[1] eq '_project';

    my $proj = checkprojrepoarch($projid, undef, undef, 1);

    if ($cgi->{'multibuild'} && @$packids[0] ne '_project' && !$BSStdServer::isajax) {
      $packids = [ BSSrcServer::Multibuild::addmultibuildpackages($projid, undef, @{$packids}) ];
    }

    if ($packids ne '_project') {
      my %knownpackids = map {$_ => 1} findpackages($projid, $proj, 1);
      for (@{$packids}) {
        die("404 unknown package '$_'\n") if !$knownpackids{$_};
      }
    }
    my @prpas;
    for my $repo (@{$proj->{'repository'} || []}) {
      my @archs = @{$repo->{'arch'} || []};
      push @prpas, map {"$projid/$repo->{'name'}/$_"} @archs;
    }

    my $ps = {};
    my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
    my @args;
    push @args, 'lastbuild' if $cgi->{'lastbuild'};
    push @args, map {"prpa=$_"} @prpas;
    push @args, map {"package=$_"} @{$packids || []};
    push @args, map {"code=$_"} @{$cgi->{'code'} || []};
    eval {
      $ps = BSWatcher::rpc("$reposerver/_result", $BSXML::resultlist, @args);
    };
    if ($@) {
      print "warning: $reposerver: $@";
      $ps = {};
    }

    push @$results, @{$ps->{'result'}};
  }
  return ({'result' => $results}, $BSXML::resultlist);
}

sub docommand {
  my ($cgi, $projid) = @_;

  my %repoidfilter = map {$_ => 1} @{$cgi->{'repository'} || []};
  my %archfilter = map {$_ => 1} @{$cgi->{'arch'} || []};

  my $proj = BSRevision::readproj_local($projid);
  if ($cgi->{'cmd'} eq 'suspendproject') {
    notify_repservers('suspendproject', $projid, undef, $cgi->{'comment'} || 'suspendproject');
    return $BSStdServer::return_ok;
  }
  if ($cgi->{'cmd'} eq 'resumeproject') {
    notify_repservers('resumeproject', $projid, undef, $cgi->{'comment'} || 'suspendproject');
    return $BSStdServer::return_ok;
  }
  my @prpas;
  for my $repo (@{$proj->{'repository'} || []}) {
    next if %repoidfilter && !$repoidfilter{$repo->{'name'}};
    my @archs = @{$repo->{'arch'} || []};
    @archs = grep {$archfilter{$_}} @archs if %archfilter;
    push @prpas, map {"$projid/$repo->{'name'}/$_"} @archs;
  }
  die("no repository defined\n") unless @prpas;
  my @packids = @{$cgi->{'package'} || []};
  if ($cgi->{'cmd'} eq 'wipepublishedlocked') {
    die("wipepublishedlocked can only wipe complete repos\n") if $cgi->{'arch'} || $cgi->{'code'} || @packids;
  }
  # check if the specified packages are known
  if (@packids) {
    @packids = BSUtil::unify(@packids);
    my %packids = map {$_ => 1} findpackages($projid, $proj, 1);
    my @badpacks = grep {!$packids{$_}} @packids;
    die("404 unknown package: @badpacks\n") if @badpacks;
  }
  # generate package list for the commands that need it
  if (!@packids && $cgi->{'cmd'} ne 'unpublish' && $cgi->{'cmd'} ne 'wipepublishedlocked') {
    @packids = findpackages($projid, $proj);
    die("no packages defined\n") unless @packids;
  }

  die("illegal wipe parameter\n") if $cgi->{'wipe'} && $cgi->{'cmd'} ne 'wipe';
  
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my $res;
  my @args;
  push @args, 'lastbuild' if $cgi->{'lastbuild'};
  push @args, map {"prpa=$_"} @prpas;
  push @args, map {"package=$_"} @packids;
  push @args, map {"code=$_"} @{$cgi->{'code'} || []};
  push @args, map {"wipe=$_"} @{$cgi->{'wipe'} || []};
  push @args, "sysrq=$cgi->{'sysrq'}" if $cgi->{'sysrq'};
  push @args, "cmd=$cgi->{'cmd'}";
  push @args, 'allarch=1' unless %archfilter;
  my $param = {
    'uri' => "$reposerver/_command",
    'request' => 'POST',
    'formurlencode' => 1,
  };
  $res = BSWatcher::rpc($param, undef, @args);
  return $res;
}

sub checkprojrepoarch {
  my ($projid, $repoid, $arch, $remoteok) = @_;
  my $proj = BSRevision::readproj_local($projid, 1);
  $proj = BSSrcServer::Remote::remoteprojid($projid) if $remoteok && (!$proj || $proj->{'remoteurl'});
  die("404 project '$projid' does not exist\n") if !$proj;
  die("404 project '$projid' is remote\n") if $proj->{'remoteurl'} && !$remoteok;
  return $proj if $proj->{'remoteurl'};
  return $proj unless defined $repoid;
  my $repo = (grep {$_->{'name'} eq $repoid} @{$proj->{'repository'} || []})[0];
  die("404 project '$projid' has no repository '$repoid'\n") unless $repo;
  return $proj unless defined $arch;
  die("404 repository '$projid/$repoid' has no architecture '$arch'\n") unless grep {$_ eq $arch} @{$repo->{'arch'} || []};
  return $proj;
}

sub getbuilddepinfo {
  my ($cgi, $projid, $repoid, $arch) = @_;

  checkprojrepoarch($projid, $repoid, $arch);
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my @args = BSRPC::args($cgi, 'package', 'view');
  my $param = {
    'uri' => "$reposerver/build/$projid/$repoid/$arch/_builddepinfo",
  };
  if (BSServer::have_content()) {
    $param->{'request'} = 'POST';
    $param->{'data'} = BSServer::read_data(10000000);
    $param->{'headers'} = [ 'Content-Type: application/octet-stream' ];
  }
  my $res = BSWatcher::rpc($param, $BSXML::builddepinfo, @args);
  return ($res, $BSXML::builddepinfo);
}

sub getjobhistory {
  my ($cgi, $projid, $repoid, $arch) = @_;

  checkprojrepoarch($projid, $repoid, $arch);
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my @args = BSRPC::args($cgi, 'limit', 'package', 'code', 'endtime_start', 'endtime_end');
  my $res = BSWatcher::rpc("$reposerver/build/$projid/$repoid/$arch/_jobhistory", $BSXML::jobhistlist, @args);
  return ($res, $BSXML::jobhistlist);
}

sub getjobhistory_project {
  my ($cgi, $projid) = @_;
  my $proj = checkprojrepoarch($projid, undef, undef, 1);
  my %repoidfilter = map {$_ => 1} @{$cgi->{'repository'} || []};
  my %archfilter = map {$_ => 1} @{$cgi->{'arch'} || []};
  my @prpas;
  for my $repo (@{$proj->{'repository'} || []}) {
    next if %repoidfilter && !$repoidfilter{$repo->{'name'}};
    my @archs = @{$repo->{'arch'} || []};
    @archs = grep {$archfilter{$_}} @archs if %archfilter;
    push @prpas, map {"$projid/$repo->{'name'}/$_"} @archs;
  }
  return ({}, $BSXML::jobhistlist) unless @prpas;
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my @args = BSRPC::args($cgi, 'limit', 'package', 'code', 'endtime_start', 'endtime_end');
  my $res = BSWatcher::rpc("$reposerver/_jobhistory", $BSXML::jobhistlist, (map {"prpa=$_"} @prpas), @args);
  return ($res, $BSXML::jobhistlist);
}

sub getpackagelist_build {
  my ($cgi, $projid, $repoid, $arch) = @_;
  my $view = $cgi->{'view'};
  if ($view) {
    die("unknown view '$view'\n") unless $view eq 'binaryversions' || $view eq 'binaryversionscode' || $view eq 'status' || $view eq 'binarychecksums' || $view eq 'binaryversionscookie';
    die("package filtering is not supported for this $view\n") if $cgi->{'package'} && $view eq 'binaryversionscookie';
    my $proj = checkprojrepoarch($projid, $repoid, $arch, 1);
    my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
    my @args = BSRPC::args($cgi, 'view', 'package');
    my $param = {
      'uri' => "$reposerver/build/$projid/$repoid/$arch",
      'ignorestatus' => 1,
      'receiver' => \&BSServer::reply_receiver,
    };
    if ($proj->{'remoteurl'}) {
      if (!$BSStdServer::isajax) {
	BSHandoff::handoff_part('interconnect_out', "/build/$projid/$repoid/$arch" , undef, @args);
      }
      if (@{$cgi->{'package'} || []} && ($view eq 'binaryversions' || $view eq 'binaryversionscode')) {
	my $pbvl = BSSrcServer::Remote::getpackagebinaryversionlist($proj, $projid, $repoid, $arch, $cgi->{'package'}, $view);
	return ($pbvl, $BSXML::packagebinaryversionlist);
      }
      if (@{$cgi->{'package'} || []} && $view eq 'binarychecksums') {
	my $pbc = BSSrcServer::Remote::getpackagebinaryversionlist($proj, $projid, $repoid, $arch, $cgi->{'package'}, $view);
	return ($pbc, $BSXML::packagebinarychecksums);
      }
      $param->{'uri'} = "$proj->{'remoteurl'}/build/$proj->{'remoteproject'}/$repoid/$arch";
      $param->{'proxy'} = $proxy;
    }
    BSWatcher::rpc($param, undef, @args);
    return undef;
  }
  return getpackagelist({ %$cgi, 'expand' => 1, 'noorigins' => 1 }, $projid, $repoid, $arch);
}

sub getbinarylist {
  my ($cgi, $projid, $repoid, $arch, $packid) = @_;

  my $view = $cgi->{'view'};
  my $nosource = $cgi->{'nosource'};
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my @args = BSRPC::args($cgi, 'view', 'nosource', 'withmd5', 'binary', 'module', 'withccache');
  if ($view && ($view eq 'cache' || $view eq 'cpio' || $view eq 'solv' || $view eq 'solvstate')) {
    # do not check arch in interconnect mode
    my $proj = checkprojrepoarch($projid, $repoid, undef, 1);
    if ($view eq 'cpio' && $packid eq '_repository' && !@{$cgi->{'binary'} || []}) {
      if (!$proj->{'remoteurl'} || $proj->{'partition'}) {
        my $repo = (grep {$_->{'name'} eq $repoid} @{$proj->{'repository'} || []})[0] || {};
        my $doddata = (grep {($_->{'arch'} || '') eq $arch} @{$repo->{'download'} || []})[0];
	die("will not get all dod packages\n") if $doddata;
      }
    }
    if (!$BSStdServer::isajax) {
      if ($proj->{'remoteurl'} && $view eq 'cpio' && $packid eq '_repository' && !$nosource && @{$cgi->{'binary'} || []}) {
	# hand over to worker_getbinaries to get the answer cached
	@args = ();
	push @args, "project=$projid";
	push @args, "repository=$repoid";
	push @args, "arch=$arch";
	push @args, "binaries=".join(',', @{$cgi->{'binary'} || []});
	push @args, map {"module=$_"} @{$cgi->{'module'} || []};
	BSHandoff::handoff_part('interconnect_out', '/getbinaries', undef, @args);
      }
      if ($proj->{'remoteurl'}) {
        BSHandoff::handoff_part('interconnect_out', "/build/$projid/$repoid/$arch/$packid", undef, @args);
      } else {
        BSHandoff::handoff_part('interconnect_in', "/build/$projid/$repoid/$arch/$packid", undef, @args);
      }
    }
    my $param = {
      'uri' => "$reposerver/build/$projid/$repoid/$arch/$packid",
      'ignorestatus' => 1,
      'receiver' => \&BSServer::reply_receiver,
    };
    if ($proj->{'remoteurl'}) {
      $param->{'uri'} = "$proj->{'remoteurl'}/build/$proj->{'remoteproject'}/$repoid/$arch/$packid";
      $param->{'proxy'} = $proxy;
    }
    BSWatcher::rpc($param, undef, @args);
    return undef;
  }
  my $proj = checkprojrepoarch($projid, $repoid, $arch, 1);
  my $param = {
    'uri' => "$reposerver/build/$projid/$repoid/$arch/$packid",
  };
  if ($proj->{'remoteurl'}) {
    $param->{'uri'} = "$proj->{'remoteurl'}/build/$proj->{'remoteproject'}/$repoid/$arch/$packid";
    $param->{'proxy'} = $proxy;
  }
  if ($view && $view eq 'binaryversions') {
    push @args, 'nometa=1' if $cgi->{'nometa'};
    push @args, 'withevr=1' if $cgi->{'withevr'};
    if (!$BSStdServer::isajax && $packid eq '_repository' && $proj->{'remoteurl'} && !$proj->{'partition'}) {
      # hand over to getbinaryversions for chunking
      @args = ();
      push @args, "project=$projid";
      push @args, "repository=$repoid";
      push @args, "arch=$arch";
      push @args, BSRPC::args($cgi, 'nometa', 'module', 'withevr');
      push @args, "binaries=".join(',', @{$cgi->{'binary'} || []});
      BSHandoff::handoff_part('interconnect_out', '/getbinaryversions', undef, @args);
    }
    if (!$BSStdServer::isajax && $packid eq '_repository') {
      my $repo = (grep {$_->{'name'} eq $repoid} @{$proj->{'repository'} || []})[0] || {};
      my $doddata = (grep {($_->{'arch'} || '') eq $arch} @{$repo->{'download'} || []})[0];
      die("will not get all dod packages\n") if $doddata && !@{$cgi->{'binary'} || []};
      my $useragent = $BSServer::request->{'headers'}->{'user-agent'} || '';
      if ($cgi->{'nometa'} && $useragent =~ /BSRPC/) {
	# probably comming from an interconnect, do a handoff
	BSHandoff::handoff_part('interconnect_in', "/build/$projid/$repoid/$arch/$packid", undef, @args);
      }
      # this can take a while if we have dod configured, in that case handoff
      BSHandoff::handoff("/build/$projid/$repoid/$arch/$packid", undef, @args) if $doddata;
    }
    my $bvl = BSWatcher::rpc($param, $BSXML::binaryversionlist, @args);
    return ($bvl, $BSXML::binaryversionlist);
  }
  if ($view && $view eq 'names' && $cgi->{'binary'} && !$BSStdServer::isajax && $packid eq '_repository') {
    my $repo = (grep {$_->{'name'} eq $repoid} @{$proj->{'repository'} || []})[0] || {};
    my $doddata = (grep {($_->{'arch'} || '') eq $arch} @{$repo->{'download'} || []})[0];
    # this can take a while if we have dod configured, in that case handoff
    BSHandoff::handoff("/build/$projid/$repoid/$arch/$packid", undef, @args) if $doddata;
  }
  if ($view && ($view eq 'cpioheaders' || $view eq 'cpioheaderchksums')) {
     $param->{'ignorestatus'} = 1;
     $param->{'receiver'} = \&BSServer::reply_receiver;
     BSWatcher::rpc($param, undef, @args);
     return undef;
  }
  my $bl = BSWatcher::rpc($param, $BSXML::binarylist, @args);
  return ($bl, $BSXML::binarylist);
}

sub getbinary {
  my ($cgi, $projid, $repoid, $arch, $packid, $filename) = @_;
  my $proj = checkprojrepoarch($projid, $repoid, $arch, 1);
  my $view = $cgi->{'view'} || '';
  if ($proj->{'remoteurl'} && $packid eq '_repository' && !$view) {
    # hack: reroute to /getbinaries so that our local cache is used
    die("need the raw package name as filename for remote repository access\n") if $filename =~ /\.(?:$binsufsre)$/;
    my @args;
    push @args, "project=$projid";
    push @args, "repository=$repoid";
    push @args, "arch=$arch";
    push @args, "binaries=$filename";
    push @args, "raw=1";
    push @args, map {"module=$_"} @{$cgi->{'module'} || []};
    BSHandoff::handoff_part('interconnect_out', '/getbinaries', undef, @args);
  }
  if ($view eq 'publishedpath') {
    die("publishedpath does not work for _repository\n") if $packid eq '_repository';
    return published_path($cgi, $projid, $repoid);
  }
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my @args = BSRPC::args($cgi, 'view', 'module', 'withfilelist');
  my $param = {
    'uri' => "$reposerver/build/$projid/$repoid/$arch/$packid/$filename",
    'ignorestatus' => 1,
    'receiver' => \&BSServer::reply_receiver,
  };
  if ($view && $view eq 'fileinfo_ext') {
    my $partition = BSSrcServer::Partition::projid2partition($projid);
    my $projpack = (getprojpack({'nopackages' => 1, 'withrepos' => 1, 'expandedrepos' => 1, 'withremotemap' => 1, 'withconfig' => 1, 'partition' => $partition}, [ $projid ], [ $repoid ], undef, $arch))[0];
    if ($projpack) {
      if ($projpack->{'project'} && $projpack->{'project'}->[0]->{'name'} eq $projid) {
        my $config = (getbuildconfig({}, $projid, $repoid))[0];
	$projpack->{'project'}->[0]->{'config'} = $config if $config;
      }
      $param->{'request'} = 'POST';
      $param->{'data'} = BSUtil::toxml($projpack, $BSXML::projpack);
      $param->{'headers'} = [ 'Content-Type: application/octet-stream' ];
    }
  }
  if ($proj->{'remoteurl'}) {
    $param->{'uri'} = "$proj->{'remoteurl'}/build/$proj->{'remoteproject'}/$repoid/$arch/$packid/$filename";
    $param->{'proxy'} = $proxy;
  }
  BSWatcher::rpc($param, undef, @args);
  return undef;
}

sub putbinary {
  my ($cgi, $projid, $repoid, $arch, $filename) = @_;
  checkprojrepoarch($projid, $repoid, $arch);
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my @args = BSRPC::args($cgi, 'ignoreolder', 'wipe');
  my $param = {
    'uri' => "$reposerver/build/$projid/$repoid/$arch/_repository/$filename",
    'request' => 'PUT',
    'data' => \&BSServer::forward_sender,
    'chunked' => 1,
  };
  # XXX add return type checking
  return BSWatcher::rpc($param, undef, @args);
}

sub delbinary {
  my ($cgi, $projid, $repoid, $arch, $filename) = @_;
  checkprojrepoarch($projid, $repoid, $arch);
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my $param = {
    'uri' => "$reposerver/build/$projid/$repoid/$arch/_repository/$filename",
    'request' => 'DELETE',
  };
  return BSWatcher::rpc($param, undef);
}

# 1: flavor was already present, 2: flavor has been added, 0: flavor could not be added
sub ensure_known_flavor {
  my ($projid, $packid, $flavor) = @_;
  my $mb = BSSrcServer::Multibuild::getmultibuild($projid, $packid) || {};
  return 1 if grep {$_ eq $flavor} @{$mb->{'flavor'} || $mb->{'package'} || []};
  # nope, try to expand, add flavor if it works and it is known
  eval {
    my $rev = getrev($projid, $packid, 'latest');
    my $files = lsrev_expanded($rev);
    my $newmb = BSSrcServer::Multibuild::getmultibuild_fromfiles($projid, $packid, $files);
    if (grep {$_ eq $flavor} @{$newmb->{'flavor'} || $newmb->{'package'} || []}) {
      BSSrcServer::Multibuild::updatemultibuild($projid, $packid, $files, 1);
      notify_repservers('package', $projid, $packid);
    }
  };
  warn($@) if $@;
  $mb = BSSrcServer::Multibuild::getmultibuild($projid, $packid) || {};
  return 2 if grep {$_ eq $flavor} @{$mb->{'flavor'} || $mb->{'package'} || []};
  return 0;
}

sub copybuild {
  my ($cgi, $projid, $repoid, $arch, $packid) = @_;
  die("illegal package '$packid'\n") if $packid =~ /^_/ && !($packid =~ /^_product:/);
  checkprojrepoarch($projid, $repoid, $arch);
  my $oprojid = defined($cgi->{'oproject'}) ? $cgi->{'oproject'} : $projid;
  my $opackid = defined($cgi->{'opackage'}) ? $cgi->{'opackage'} : $packid;
  my $orepoid = defined($cgi->{'orepository'}) ? $cgi->{'orepository'} : $repoid;
  die("nothing to do\n") if "$oprojid/$opackid/$orepoid" eq "$projid/$packid/$repoid";
  checkprojrepoarch($oprojid, $orepoid, $arch);
  # make sure the packages exist. not cheap, but does everything we need
  getrev($projid, $packid);
  getrev($oprojid, $opackid);
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my $oreposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($oprojid) : $BSConfig::reposerver;
  my %tocopy = ($opackid => $packid);
  if ($cgi->{'multibuild'}) {
    my $mb = BSSrcServer::Multibuild::getmultibuild($oprojid, $opackid) || {};
    $tocopy{"$opackid:$_"} = "$packid:$_" for @{$mb->{'flavor'} || $mb->{'package'} || []};
  }
  for $opackid (sort keys %tocopy) {
    next unless $tocopy{$opackid} =~ /(?<!^_product)(?<!^_patchinfo):./ && $tocopy{$opackid} =~ /^(.*):([^:]*)$/s;
    my ($mbpackid, $flavor) = ($1, $2);
    ensure_known_flavor($projid, $mbpackid, $flavor);
  }
  for $opackid (sort keys %tocopy) {
    my @args;
    push @args, "cmd=copy";
    push @args, "oproject=$oprojid";
    push @args, "opackage=$opackid";
    push @args, "orepository=$orepoid";
    push @args, "setupdateinfoid=$cgi->{'setupdateinfoid'}" if $cgi->{'setupdateinfoid'};
    push @args, "setrelease=$cgi->{'setrelease'}" if $cgi->{'setrelease'};
    push @args, 'resign=1' if $cgi->{'resign'};
    push @args, "remoteurl=$oreposerver" if $reposerver ne $oreposerver;
    my $param = {
      'uri' => "$reposerver/build/$projid/$repoid/$arch/$tocopy{$opackid}",
      'request' => 'POST',
    };
    BSWatcher::rpc($param, undef, @args);
  }
  return $BSStdServer::return_ok;
}

sub uploadbuild {
  my ($cgi, $projid, $repoid, $arch, $packid) = @_;
  die("illegal package '$packid'\n") if $packid =~ /^_/ && !($packid =~ /^_product:/);
  checkprojrepoarch($projid, $repoid, $arch);
  if ($packid =~ /(?<!^_product)(?<!^_patchinfo):./ && $packid =~ /^(.*):([^:]*)$/s) {
    # make sure the flavor is known
    my ($mbpackid, $flavor) = ($1, $2);
    die("404 unknown multibuild flavor '$flavor'\n") unless ensure_known_flavor($projid, $mbpackid, $flavor);
  }
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my $param = {
    'uri' => "$reposerver/build/$projid/$repoid/$arch/$packid",
    'request' => 'POST',
    'data' => \&BSServer::forward_sender,
    'chunked' => 1,
  };
  # XXX add return type checking
  return BSWatcher::rpc($param, undef);
}


sub getlogfile {
  my ($cgi, $projid, $repoid, $arch, $packid) = @_;
  checkprojrepoarch($projid, $repoid, $arch);

  if (!$cgi->{'start'}) {
    # check if the package is broken
    my $rev = getrev($projid, $packid, 'build');
    eval {
      lsrev_expanded($rev);
    };
    if ($@) {
      my $error = $@;
      if ($rev->{'srcmd5'}) {
        my $files = BSRevision::lsrev($rev);
	if ($files->{'_serviceerror'}) {
	  $error = BSRevision::revreadstr($rev, '_serviceerror', $files->{'_serviceerror'});
	} elsif ($files->{'_linkerror'}) {
	  $error = BSRevision::revreadstr($rev, '_linkerror', $files->{'_linkerror'});
	}
      }
      if ($cgi->{'view'} && $cgi->{'view'} eq 'entry') {
        my $entry = {'name' => '_log', 'size' => length($error)};
        return ({'entry' => [ $entry ]}, $BSXML::dir);
      }
      return $error;
    }
  }

  my @args = BSRPC::args($cgi, 'last', 'lastsucceeded', 'nostream', 'start', 'end', 'view');
  if (!$BSStdServer::isajax && !$cgi->{'view'}) {
    BSHandoff::handoff("/build/$projid/$repoid/$arch/$packid/_log", undef, @args);
  }
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my $param = {
    'uri' => "$reposerver/build/$projid/$repoid/$arch/$packid/_log",
    'ignorestatus' => 1,
    'receiver' => \&BSServer::reply_receiver,
    'joinable' => 1,
  };
  BSWatcher::rpc($param, undef, @args);
  return undef; # always streams result
}

sub getjobstatus {
  my ($cgi, $projid, $repoid, $arch, $packid) = @_;
  checkprojrepoarch($projid, $repoid, $arch);
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my $jobstatus = BSWatcher::rpc("$reposerver/build/$projid/$repoid/$arch/$packid/_jobstatus", $BSXML::jobstatus);
  return ($jobstatus, $BSXML::jobstatus);
}

sub getbuildhistory {
  my ($cgi, $projid, $repoid, $arch, $packid) = @_;
  checkprojrepoarch($projid, $repoid, $arch);
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my @args = BSRPC::args($cgi, 'limit');
  my $buildhist = BSWatcher::rpc("$reposerver/build/$projid/$repoid/$arch/$packid/_history", $BSXML::buildhist, @args);
  return ($buildhist, $BSXML::buildhist);
}

sub getbuildstats {
  my ($cgi, $projid, $repoid, $arch, $packid) = @_;
  checkprojrepoarch($projid, $repoid, $arch);
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my @args = BSRPC::args($cgi, 'limit');
  my $buildstats = BSWatcher::rpc("$reposerver/build/$projid/$repoid/$arch/$packid/_buildstats", $BSXML::buildstatslist, @args);
  return ($buildstats, $BSXML::buildstatslist);
}

sub getbuildinfo {
  my ($cgi, $projid, $repoid, $arch, $packid) = @_;
  checkprojrepoarch($projid, $repoid, $arch, 1);
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my @args = BSRPC::args($cgi, 'internal', 'debug', 'add');
  my $buildinfo = BSWatcher::rpc("$reposerver/build/$projid/$repoid/$arch/$packid/_buildinfo", $BSXML::buildinfo, @args);
  if ($cgi->{'striphdrmd5'}) {
    # FIXME3.0: workaround broken osc python3 versions not able to deal with hdrmd5 correctly
    for (@{$buildinfo->{'bdep'} || []}) {
      delete $_->{'hdrmd5'} unless $_->{'name'} =~ /^container:/;
    }
  }
  return ($buildinfo, $BSXML::buildinfo);
}

sub getbuildinfo_post {
  my ($cgi, $projid, $repoid, $arch, $packid) = @_;
  checkprojrepoarch($projid, $repoid, $arch, 1);
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my @args = BSRPC::args($cgi, 'debug', 'add');
  my $param = {
    'uri' => "$reposerver/build/$projid/$repoid/$arch/$packid/_buildinfo",
    'request' => 'POST',
    'data' => \&BSServer::forward_sender,
    'chunked' => 1,
  };
  my $buildinfo = BSWatcher::rpc($param, $BSXML::buildinfo, @args);
  if ($cgi->{'striphdrmd5'}) {
    # FIXME3.0: workaround broken osc python3 versions not able to deal with hdrmd5 correctly
    for (@{$buildinfo->{'bdep'} || []}) {
      delete $_->{'hdrmd5'} unless $_->{'name'} =~ /^container:/;
    }
  }
  return ($buildinfo, $BSXML::buildinfo);
}

sub getbuildreason {
  my ($cgi, $projid, $repoid, $arch, $packid) = @_;
  checkprojrepoarch($projid, $repoid, $arch);
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my $reason = BSWatcher::rpc("$reposerver/build/$projid/$repoid/$arch/$packid/_reason", $BSXML::buildreason);
  return ($reason, $BSXML::buildreason);
}

sub getmultibuildpackages {
  my ($cgi, $projid, $packid) = @_;
  my $pack_hash = {};
  my $mb = BSSrcServer::Multibuild::getmultibuild($projid, $packid) || {};
  for my $pack (@{$mb->{'flavor'} || $mb->{'package'} || []}) {
    my $entry = {'name' => $pack};
    push @{$pack_hash->{'entry'}}, $entry;
  }
  return ($pack_hash, $BSXML::dir);
}

sub getbuildstatus {
  my ($cgi, $projid, $repoid, $arch, $packid) = @_;
  checkprojrepoarch($projid, $repoid, $arch);
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my $status = BSWatcher::rpc("$reposerver/build/$projid/$repoid/$arch/$packid/_status", $BSXML::buildstatus);
  return ($status, $BSXML::buildstatus);
}

sub add_daemondata {
  my ($ws, $daemondata) = @_;
  my $part = (grep {!defined($_->{'name'})} @{$ws->{'partition'} || []})[0];
  if (!$part) {
    $part = {};
    $ws->{'partition'} ||= [];
    unshift @{$ws->{'partition'}}, $part;
  }
  $part->{'daemon'} ||= [];
  unshift(@{$part->{'daemon'}}, $daemondata);
  return $ws;
}

sub add_localworkerstatus {
  my ($cgi, $ws, $type, $lock) = @_;
  return $ws if $cgi->{'type'} && !grep {$_ eq $type} @{$cgi->{'type'}};
  return $ws if $cgi->{'arch'} && !grep {$_ eq $type} @{$cgi->{'arch'}};
  return $ws if $type eq 'servicedispatch' && !$BSConfig::servicedispatch;
  return $ws if $type eq 'redis' && !$BSConfig::redisserver;
  return $ws if $type eq 'sourcepublish' && !$BSConfig::sourcepublish_sync;
  return $ws if $type ne 'srcserver' && ! -e $lock;
  my $daemondata = {'state' => 'dead', 'type' => $type};
  if ($type eq 'srcserver') {
    my $req = $BSServer::request;
    $daemondata->{'starttime'} = $req->{'server'}->{'starttime'} if $req && $req->{'server'};
    if ($req && $req->{'conf'} && $req->{'conf'}->{'handoffpath'}) {
      $lock = "$req->{'conf'}->{'handoffpath'}.lock";
    }
    $daemondata->{'state'} = 'running' unless $lock;
  }
  local *F;
  if ($lock && open(F, '<', $lock)) {
    if (!flock(F, LOCK_EX | LOCK_NB)) {
      my @s = stat(F);
      $daemondata->{'state'} = 'running';
      $daemondata->{'starttime'} ||= $s[9] if @s;
    }
    close F;
  }
  return add_daemondata($ws, $daemondata);
}

sub add_serviceserverstatus {
  my ($cgi, $ws) = @_;
  return $ws unless $BSConfig::serviceserver;
  return $ws if $cgi->{'type'} && !grep {$_ eq 'service'} @{$cgi->{'type'}};
  return $ws if $cgi->{'arch'} && !grep {$_ eq 'service'} @{$cgi->{'arch'}};
  my $daemondata = {'state' => 'dead', 'type' => 'service'};
  my $param = {
    'uri' => "$BSConfig::serviceserver/serverstatus",
    'timeout' => 60,
  };
  eval {
    my $sta = BSWatcher::rpc($param, $BSXML::serverstatus);
    $daemondata->{'state'} = 'running';
    $daemondata->{'starttime'} = $sta->{'starttime'};
  };
  warn($@) if $@;
  return add_daemondata($ws, $daemondata);
}

sub getworkerstatus {
  my ($cgi) = @_;
  # compat
  if ($cgi->{'scheduleronly'} && !$cgi->{'daemonsonly'}) {
    $cgi->{'daemonsonly'} = delete $cgi->{'scheduleronly'};
  }
  my @args = BSRPC::args($cgi, 'daemonsonly', 'arch');

  if (!$BSConfig::partitioning || !$BSConfig::partitionservers) {
    my $reposerver = $BSConfig::reposerver;
    my $param = {
      'uri' => "$reposerver/workerstatus",
      'timeout' => 60,
    };
    my $ws = { 'partition' => [ { 'daemon' => [ { 'state' => 'dead', 'type' => 'repserver' } ] } ] };
    eval {
      $ws = BSWatcher::rpc($param, $BSXML::workerstatus, @args);
    };
    warn($@) if $@;
    delete $_->{'uri'} for @{$ws->{'idle'} || []};
    delete $_->{'uri'} for @{$ws->{'building'} || []};
    add_serviceserverstatus($cgi, $ws);
    add_localworkerstatus($cgi, $ws, 'deltastore', "$rundir/bs_deltastore.lock");
    add_localworkerstatus($cgi, $ws, 'servicedispatch', "$rundir/bs_servicedispatch.lock");
    add_localworkerstatus($cgi, $ws, 'redis', "$rundir/bs_redis.lock");
    add_localworkerstatus($cgi, $ws, 'sourcepublish', "$rundir/bs_sourcepublish.lock");
    add_localworkerstatus($cgi, $ws, 'srcserver');
    return ($ws, $BSXML::workerstatus);
  }

  # cummulated worker status
  my $cws;
  if (!$cgi->{'daemonsonly'}) {
    $cws->{'clients'} = 0;
    $cws->{'building'} = [];
    $cws->{'waiting'} = [];
    $cws->{'blocked'} = [];
    $cws->{'buildavg'} = [];
    $cws->{'idle'} = [];
  }
  $cws->{'partition'} = [];

  my %reposervers = map {$_ => 1} values(%$BSConfig::partitionservers);
  for my $reposerver (sort keys %reposervers) {
    my $param = {
      'uri' => "$reposerver/workerstatus",
      'timeout' => 60,
    };
    my $ws;
    eval {
      $ws = BSWatcher::rpc("$reposerver/workerstatus", $BSXML::workerstatus, @args);
    };
    warn($@) if $@;
    if (!$ws) {
      for my $par (grep {$BSConfig::partitionservers->{$_} eq $reposerver} sort(keys(%$BSConfig::partitionservers))) {
        push @{$ws->{'partition'}}, { 'name' => $par, 'daemon' => [ { 'state' => 'dead', 'type' => 'repserver' } ] };
      }
    }

    push @{$cws->{'partition'}}, @{$ws->{'partition'}};
    next if $cgi->{'daemonsonly'};

    delete $_->{'uri'} for @{$ws->{'idle'} || []};
    delete $_->{'uri'} for @{$ws->{'building'} || []};
    for my $workerstate (qw{idle building down dead away}) {
      push @{$cws->{$workerstate}}, @{$ws->{$workerstate}} if $ws->{$workerstate};
    }
    for my $b (@{$ws->{'waiting'} || []}) {
      my $ob = (grep {$_->{'arch'} eq $b->{'arch'}} @{$cws->{'waiting'} || []})[0];
      if (!$ob) {
        $ob = {'arch' => $b->{'arch'}, 'jobs' => 0};
        push @{$cws->{'waiting'}}, $ob;
      }
      $ob->{'jobs'} += $b->{'jobs'};
    }
    for my $b (@{$ws->{'blocked'} || []}) {
      my $ob = (grep {$_->{'arch'} eq $b->{'arch'}} @{$cws->{'blocked'} || []})[0];
      if (!$ob) {
        $ob = {'arch' => $b->{'arch'}, 'jobs' => 0};
        push @{$cws->{'blocked'}}, $ob;
      }
      $ob->{'jobs'} += $b->{'jobs'};
    }
    for my $b (@{$ws->{'buildavg'} || []}) {
      my $ob = (grep {$_->{'arch'} eq $b->{'arch'}} @{$cws->{'buildavg'} || []})[0];
      if (!$ob) {
        $ob = {'arch' => $b->{'arch'}, 'buildavg' => 0, 'count' => 0};
        push @{$cws->{'buildavg'}}, $ob;
      }
      $ob->{'buildavg'} += $b->{'buildavg'};
      $ob->{'count'} += 1;
    }
    $cws->{'clients'} += $ws->{'clients'} if $ws->{'clients'};
  }
  for my $b (@{$cws->{'buildavg'} || []}) {
    $b->{'buildavg'} /= delete $b->{'count'};
  }

  # sort
  if (!$cgi->{'daemonsonly'}) {
    $cws->{'idle'} = [ sort {$a->{'workerid'} cmp $b->{'workerid'} || $a->{'uri'} cmp $b->{'uri'} || $a cmp $b} @{$cws->{'idle'}} ];
    $cws->{'building'} = [ sort {$a->{'workerid'} cmp $b->{'workerid'} || $a->{'uri'} cmp $b->{'uri'} || $a cmp $b} @{$cws->{'building'}} ];
    $cws->{'waiting'} = [ sort {$a->{'arch'} cmp $b->{'arch'} || $a cmp $b}  @{$cws->{'waiting'}} ];
    $cws->{'blocked'} = [ sort {$a->{'arch'} cmp $b->{'arch'} || $a cmp $b}  @{$cws->{'blocked'}} ];
    $cws->{'buildavg'} = [ sort {$a->{'arch'} cmp $b->{'arch'} || $a cmp $b}  @{$cws->{'buildavg'}} ];
  }
  
  add_serviceserverstatus($cgi, $cws);
  add_localworkerstatus($cgi, $cws, 'deltastore', "$rundir/bs_deltastore.lock");
  add_localworkerstatus($cgi, $cws, 'servicedispatch', "$rundir/bs_servicedispatch.lock");
  add_localworkerstatus($cgi, $cws, 'redis', "$rundir/bs_redis.lock");
  add_localworkerstatus($cgi, $cws, 'sourcepublish', "$rundir/bs_sourcepublish.lock");
  add_localworkerstatus($cgi, $cws, 'srcserver');
  return ($cws, $BSXML::workerstatus);
}

sub getajaxstatuslist {
  my ($cgi) = @_;
  my $conf = $BSServer::request->{'conf'};
  my $param = { 'uri' => '/ajaxstatus' };
  my @ajaxstatuslist;
  for (0 .. scalar(@{$conf->{'ajaxpartitions'} || []})) {
    $param->{'handoffpath'} = BSStdServer::get_handoffpath_aidx($conf, $_ || '');
    push @ajaxstatuslist, BSHandoff::rpc($param, $BSXML::ajaxstatus);
  }
  return ({ 'ajaxstatus' => \@ajaxstatuslist }, $BSXML::ajaxstatuslist);
}

sub getajaxstatus {
  my ($cgi) = @_;
  my $conf = $BSServer::request->{'conf'};
  if (!$BSStdServer::isajax) {
    my $param = { 'uri' => '/ajaxstatus' };
    $param->{'handoffpath'} = BSStdServer::get_handoffpath_aidx($conf, $cgi->{'aidx'});
    BSHandoff::handoff($param);
  }
  my $r = BSWatcher::getstatus();
  $r->{'aidx'} = $conf->{'aidx'} if $conf->{'aidx'};
  return ($r, $BSXML::ajaxstatus);
}

####################################################################

sub search_proj {
  my ($cgi, $match, $id) = @_;
  $match =~ s/^\[(.*)\]$/$1/s;
  my $data = [];
  for my $projid (findprojects()) {
    my $proj = BSRevision::readproj_local($projid);
    push @$data, $proj;
  }
  $data = BSXPath::match($data, $match);
  if ($id) {
    for (@{$data || []}) {
      $_ = {'name' => $_->{'name'}};
    }
  }
  my $res = {'project' => $data};
  return ($res, $BSXML::collection);
}

sub pkgsearch_fetch {
  my ($db, $k) = @_;
  my ($projid, $packid) = split('/', $k, 2);
  my $pack = BSRevision::readpack_local($projid, $packid, 1) || {'name' => $packid};
  $pack->{'project'} = $projid;
  #my $linkinfo = $db->rawfetch($k);
  #$pack->{'linkinfo'} = $linkinfo if $linkinfo;
  return $pack;
}

sub pkgsearch_indexfunc {
  my ($db, $path, $value, $lkeys, $hint, $hintval) = @_;
  if (!defined($path)) {
    return @{$db->{'_allkeys'}} if $db->{'_allkeys'};
    my @projids = findprojects();
    my @r;
    for my $projid (@projids) {
      push @r, map {"$projid/$_"} BSRevision::lspackages_local($projid);
    }
    $db->{'_allkeys'} = \@r;
    return @r;
  }
  if ($path =~ /^linkinfo\/(.*)/) {
    if (!defined($value)) {
      return $db->rawvalues($1, $hint, $hintval);
    } else {
      return $db->rawkeys($1, $value);
    }
  }
  if ($path eq 'project') {
    return findprojects() unless defined($value);
    return map {"$value/$_"} BSRevision::lspackages_local($value);
  }
  if ($path eq 'name') {
    $lkeys = [ pkgsearch_indexfunc($db) ] unless $lkeys;
    if (!defined($value)) {
      my %v = map {$_ => 1} grep {s/^.*\///} map {$_} @$lkeys;
      return sort keys %v;
    } else {
      return grep {/\Q$value\E$/} @$lkeys;
    }
  }
  return ();
}

sub pkgsearch_key_to_project {
  my ($db, $key) = @_;
  return (split('/', $key, 2))[0];
}

sub pkgsearch_key_to_package {
  my ($db, $key) = @_;
  return (split('/', $key, 2))[1];
}

sub search_pack {
  my ($cgi, $match, $id) = @_;
  $match =~ s/^\[(.*)\]$/$1/s;
  # really ugly hack to speed up needed api call
  if ($match =~ /^\@project='(.+)' and starts-with\(\@name,'(.+)'\)$/) {
    my $projid = $1;
    my $startswith = $2;
    $projid =~ s/''/'/g;
    $startswith =~ s/''/'/g;
    my @packages = BSRevision::lspackages_local($projid);
    my $data = [];
    for my $packid (grep {/^\Q$startswith\E/} @packages) {
      my ($pack, undef) = getpackage($cgi, $projid, $packid);
      $pack->{'project'} = $projid;
      push @$data, $pack;
    }
    my $res = {'package' => $data};
    return ($res, $BSXML::collection);
  }
  my $db = BSSrcServer::LinkinfoDB::openlinkinfodb();
  # more really ugly hacks to speed up needed api calls
  if ($id) {
    my ($packid, $projid, $limitprojid);
    if ($match =~ /^\(linkinfo\/\@package=\"([^\"]+)\" and linkinfo\/\@project=\"([^\"]+)\"\)$/s) {
      ($packid, $projid) = ($1, $2);
    } elsif ($match =~ /^\(linkinfo\/\@package=\"([^\"]+)\" and linkinfo\/\@project=\"([^\"]+)\" and \@project=\"([^\"]+)\"\)$/s) {
      ($packid, $projid, $limitprojid) = ($1, $2, $3);
    }
    if ($projid) {
      my $data;
      if ($limitprojid && $projid eq $limitprojid) {
	$data = [ map { { 'name' => $_, 'project' => $projid} } BSSrcServer::LinkinfoDB::getlocallinks($projid, $packid) ];
      } else {
	$data = [ BSSrcServer::LinkinfoDB::getlinkers($projid, $packid) ];
	$data = [ grep {/^\Q$limitprojid\E\//} @$data ]  if $limitprojid;
        for (@$data) {
	  my @p = split('/', $_, 2);
	  $_ = {'name' => $p[1], 'project' => $p[0]};
        }
      }
      my $res = {'package' => $data};
      return ($res, $BSXML::collection);
    }
  }
  $db->{'indexfunc'} = {
    'project' => \&pkgsearch_indexfunc,
    'name' => \&pkgsearch_indexfunc,
    'linkinfo/project' => \&pkgsearch_indexfunc,
    'linkinfo/package' => \&pkgsearch_indexfunc,
    'linkinfo/rev' => \&pkgsearch_indexfunc,
  };
  $db->{'allkeyspath'} = 'project';
  $db->{'noindexatall'} = 1;
  $db->{'fetch'} = \&pkgsearch_fetch;
  $db->{'fetch_project'} = \&pkgsearch_key_to_project;
  $db->{'fetch_name'} = \&pkgsearch_key_to_package;
  my $data = BSXPathKeys::node($db, '');
  if ($id) {
    $data = $data->keymatch($match);
    for (@$data) {
      my @p = split('/', $_, 2);
      $_ = {'name' => $p[1], 'project' => $p[0]};
    }
  } else {
    $data = BSXPath::match($data, $match);
    delete $_->{'linkinfo'} for @$data;
  }
  my $res = {'package' => $data};
  return ($res, $BSXML::collection);
}

sub search_proj_id {
  return search_proj(@_, 1);
}

sub search_pack_id {
  return search_pack(@_, 1);
}

#############################################################################

sub search_published_updatedb {
  my ($cgi) = @_;
  die("unknown command '$cgi->{'cmd'}'\n") unless $cgi->{'cmd'} eq 'updatedb';
  my $data = BSServer::read_data();
  $data = Storable::thaw($data);
  die("no data\n") unless $data && @$data;
  if ($BSConfig::published_db_sqlite) {
    my $db = BSSrcServer::SQLite::opendb($extrepodb, 'binary');
    while (@$data) {
      my ($w, $k, $v) = splice(@$data, 0, 3);
      if ($w eq 'repoinfo') {
        $db->updatedb_repoinfo($k, $v);
      } elsif ($w eq 'patterninfo') {
        $db->updatedb_patterninfo($k, $v);
      } elsif ($w ne 'binary' && $w ne 'pattern') {
        die("bad data type: '$w'\n");
      }
    }
    return $BSStdServer::return_ok;
  }
  my $patterndb;
  my $binarydb;
  my $repoinfodb;
  mkdir_p($extrepodb) unless -d $extrepodb;
  while (@$data) {
    my ($w, $k, $v) = splice(@$data, 0, 3);
    if ($w eq 'binary') {
      $binarydb = BSDB::opendb($extrepodb, 'binary') unless $binarydb;
      $binarydb->updateindex_rel($k || [], $v || []);
    } elsif ($w eq 'pattern') {
      $patterndb = BSDB::opendb($extrepodb, 'pattern') unless $patterndb;
      $patterndb->store($k, $v);
    } elsif ($w eq 'repoinfo') {
      if (!$repoinfodb) {
        $repoinfodb = BSDB::opendb($extrepodb, 'repoinfo');
        $repoinfodb->{'noindexatall'} = 1;
      }
      $repoinfodb->store($k, $v);
    } elsif ($w eq 'patterninfo') {
      # ignore the patterninfo for now
    } else {
      die("bad data type: '$w'\n");
    }
  }
  return $BSStdServer::return_ok;
}

#sub search_published_id {
#  my ($cgi, $what, $match) = @_;
#  my $res;
#  for my $rrserver ($BSConfig::reposerver) {
#    $res = BSRPC::rpc("$rrserver/search/published/$what/id", $BSXML::collection, "match=$match");
#    last if $res;
#  }
#  return ($res, $BSXML::collection);
#}
#
#sub search_published_binary_id {
#  return search_published_id($_[0], 'binary', $_[1]);
#}
#
#sub search_published_pattern_id {
#  return search_published_id($_[0], 'pattern', $_[1]);
#}

my %prp_to_repoinfo;
my %prp_to_repoorigins;

# map a prp to the published repoinfo
# note that the repository name may have an appended debug suffix!
sub prp_to_repoinfo {
  my ($prp, $binarydb) = @_;

  my $repoinfo = $prp_to_repoinfo{$prp};
  return $repoinfo if $repoinfo;
  if ($BSConfig::published_db_sqlite) {
    my $prp_ext = $prp;
    $prp_ext =~ s/:/:\//g;
    $binarydb ||= BSSrcServer::SQLite::opendb($extrepodb, 'binary');
    $repoinfo = $binarydb->getrepoinfo($prp_ext) || {};
  } else {
    my $repoinfodb = BSDB::opendb($extrepodb, 'repoinfo');
    $repoinfo = $repoinfodb->fetch($prp) || {};
    $prp_to_repoorigins{$prp} = delete($repoinfo->{'binaryorigins'}) || {}
  }
  for (@{$repoinfo->{'prpsearchpath'} || []}) {
    next if ref($_);	# legacy
    my ($p, $r) = split('/', $_, 2);
    $_ = {'project' => $p, 'repository' => $r};
  }
  $prp_to_repoinfo{$prp} = $repoinfo;
  return $repoinfo;
}

sub prp_to_repoorigins {
  my ($prp, $binarydb) = @_;

  my $repoorigins = $prp_to_repoorigins{$prp};
  return $repoorigins if $repoorigins;

  if ($BSConfig::published_db_sqlite) {
    my $prp_ext = $prp;
    $prp_ext =~ s/:/:\//g;
    $binarydb ||= BSSrcServer::SQLite::opendb($extrepodb, 'binary');
    $repoorigins = $binarydb->getrepoorigins($prp_ext);
  } else {
    prp_to_repoinfo($prp);
    return $prp_to_repoorigins{$prp};
  }
  $prp_to_repoorigins{$prp} = $repoorigins;
  return $repoorigins;
}

sub binary_key_to_project {
  my ($db, $key) = @_;
  my @p = split('/', $key);
  while (@p > 1 && $p[0] =~ /:$/) {
    splice(@p, 0, 2, "$p[0]$p[1]");
  }
  return shift @p;
}

sub binary_key_to_repository {
  my ($db, $key) = @_;
  my @p = split('/', $key);
  while (@p > 1 && $p[0] =~ /:$/) {
    splice(@p, 0, 2, "$p[0]$p[1]");
  }
  shift(@p);
  while (@p > 1 && $p[0] =~ /:$/) {
    splice(@p, 0, 2, "$p[0]$p[1]");
  }
  return shift @p;
}

sub binary_key_to_name {
  my ($db, $key) = @_;
  $key =~ s/.*\///;
  return $1 if $key =~ /^(.*)-[^-]+-[^-]+\.[^\.]+\.rpm$/;
  return $1 if $key =~ /^(.*)_[^_]+_[^_]+\.deb$/;
  return $1 if $key =~ /^(.*)-[^-]+-[^-]+-[^-]+\.pkg\.tar\.(?:xz|gz|zst)$/;
  return ();
}

sub binary_key_to_type {
  my ($db, $key) = @_;
  return 'rpm' if $key =~ /\.rpm$/;
  return 'deb' if $key =~ /\.deb$/;
  return 'arch' if $key =~ /\.pkg\.tar\.(?:xz|gz|zst)$/;
  return ();
}

sub binary_key_to_data {
  my ($db, $key) = @_;
  my @p = split('/', $key);
  my $binary = pop(@p);
  my $name = $binary;
  my $versrel = '';
  if ($name =~ s/-([^-]+-[^-]+)\.[^\.]+\.rpm$//) {
    $versrel = $1;
  } elsif ($name =~ s/_([^_]+)_[^_]+\.deb$//) {
    $versrel = $1;
  } elsif ($name =~ s/-([^-]+-[^-]+)-[^-]+\.pkg\.tar\.(?:xz|gz|zst)$//) {
    $versrel = $1;
  }
  my ($version, $release) = ($versrel, undef);
  ($version, $release) = ($1, $2) if $version =~ /^(.*)-(.*?)$/;
  my $arch = pop(@p);
  $arch = $1 if ($arch eq 'RPMS' || $arch eq 'SRPMS') && $binary =~ /\.([^\.]+)\.rpm$/;
  while (@p > 1 && $p[0] =~ /:$/) {
    splice(@p, 0, 2, "$p[0]$p[1]");
  }
  my $project = shift(@p);
  while (@p > 1 && $p[0] =~ /:$/) {
    splice(@p, 0, 2, "$p[0]$p[1]");
  }
  my $repository = shift(@p);
  my $prp = "$project/$repository";
  my $repoinfo = $prp_to_repoinfo{$prp} || prp_to_repoinfo($prp, $db);
  my $type;
  $type = 'rpm' if $binary =~ /\.rpm$/;
  $type = 'deb' if $binary =~ /\.deb$/;
  $type = 'arch' if $binary =~ /\.pkg\.tar\.(?:xz|gz|zst)$/;
  my $res = {
    'name' => $name,
    'versrel' => $versrel,
    'version' => $version,
    'arch' => $arch,
    'type' => $type,
    'project' => $project,
    'repository' => $repository,
    'filename' => $binary,
    'filepath' => $key,
  };
  $res->{'release'} = $release if defined $release;
  $res->{'path'} = $repoinfo->{'prpsearchpath'} if $repoinfo->{'prpsearchpath'};
  my $location = join('/', @p, $arch, $binary);
  my $package;
  if ($db->{'key2package'} && exists($db->{'key2package'}->{$key})) {
    $package = $db->{'key2package'}->{$key};
  } else {
    my $repoorigins = $prp_to_repoorigins{$prp} || prp_to_repoorigins($prp, $db);
    $package = $repoorigins->{$location};
  }
  $res->{'package'} = $package if defined $package;
  if ($repoinfo->{'base'}) {
    $res->{'baseproject'} = $repoinfo->{'base'}->{'project'};
  } elsif ($res->{'path'}) {
    $res->{'baseproject'} = $res->{'path'}->[-1]->{'project'};
  }
  return $res;
}

sub pattern_key_to_data {
  my ($db, $key) = @_;
  my @p = split('/', $key);
  my $filename = pop(@p);
  while (@p > 1 && $p[0] =~ /:$/) {
    splice(@p, 0, 2, "$p[0]$p[1]");
  }
  my $project = shift(@p);
  while (@p > 1 && $p[0] =~ /:$/) {
    splice(@p, 0, 2, "$p[0]$p[1]");
  }
  my $repository = shift(@p);
  my $res;
  if ($BSConfig::published_db_sqlite) {
    my $location = join('/', @p, $filename);
    my $prp_ext = "$project/$repository";
    $prp_ext =~ s/:/:\//g;
    $res = $db->getrecord($prp_ext, $location);
  } else {
    $res = $db->rawfetch($key);
  }
  $res ||= {};
  $res->{'baseproject'} = $res->{'path'}->[-1]->{'project'} if $res->{'path'};
  $res->{'project'} = $project;
  $res->{'repository'} = $repository;
  $res->{'filename'} = $filename;
  $res->{'filepath'} = $key;
  return $res;
}

sub published_binary_projectindexfunc {
  my ($db, $path, $value) = @_;

  return $db->getprojectkeys($value) if $BSConfig::published_db_sqlite;

  return findprojects() unless defined $value;
  my $proj = BSRevision::readproj_local($value);
  return () unless $proj;
  my @repoids = map {$_->{'name'}} @{$proj->{'repository'} || []};
  my %bins;
  for my $repoid (@repoids) {
    my $dbgsplit;
    while (1) {
      my $prp = "$value/$repoid";
      my $prp_ext = $prp;
      $prp_ext =~ s/:/:\//g;
      my $repoinfo = $prp_to_repoinfo{$prp} || prp_to_repoinfo($prp, $db);
      my $repoorigins = $prp_to_repoorigins{$prp} || prp_to_repoorigins($prp, $db);
      for (keys %{$repoorigins || {}}) {
	# keep in sync with updatebinaryindex in bs_publish
	next unless /\.(?:rpm|deb|pkg\.tar\.gz|pkg\.tar\.xz|pkg\.tar\.zst)$/;
	$bins{"$prp_ext/$_"} = 1;
      }
      last if defined $dbgsplit;
      $dbgsplit = $repoinfo->{'splitdebug'};
      last unless $dbgsplit;
      $repoid .= $dbgsplit;
    }
  }
  return sort keys %bins;
}

sub search_add_downloadurl {
  my ($data) = @_;
  for my $d (@$data) {
    my @p = split('/', $d->{'filepath'});
    my $downloadurl = BSUrlmapper::get_downloadurl("$d->{'project'}/$d->{'repository'}");
    next unless $downloadurl;
    $downloadurl =~ s/\/$//;
    $d->{'downloadurl'} = "$downloadurl/".join('/', splice(@p, -2));
  }
}

sub search_published_binary_id {
  my ($cgi, $match, $name) = @_;
  my $binarydb;
  if ($BSConfig::published_db_sqlite) {
    $binarydb = BSSrcServer::SQLite::opendb($extrepodb, 'binary');
  } else {
    $binarydb = BSDB::opendb($extrepodb, 'binary');
  }
  $binarydb->{'allkeyspath'} = sub {die("413 refusing to get all keys\n")};
  $binarydb->{'noindex'} = {'version' => 1, 'release' => 1, 'versrel' => 1, 'arch' => 1, 'project' => 1, 'repository' => 1, 'package' => 1, 'type' => 1, 'path/project' => 1, 'path/repository' => 1, 'baseproject' => 1};
  $binarydb->{'indexfunc'} = {'project' => \&published_binary_projectindexfunc };
  $binarydb->{'fetch'} = \&binary_key_to_data;
  $binarydb->{'fetch_project'} = \&binary_key_to_project;
  $binarydb->{'fetch_repository'} = \&binary_key_to_repository;
  $binarydb->{'fetch_name'} = \&binary_key_to_name;
  $binarydb->{'fetch_type'} = \&binary_key_to_type;
  $binarydb->{'cheapfetch'} = 1;
  my $limit = defined($cgi->{'limit'}) ? $cgi->{'limit'} : 1000;
  my $rootnode = BSXPathKeys::node($binarydb, '', $limit && $limit < 10 ? 1000 : $limit * 100);
  my $data;
  if ($name) {
    $data = $rootnode->keymatch($match) || [];
    $_ = binary_key_to_name($binarydb, $_) for @$data;
    my %names = map { $_ => 1 } grep {defined($_)} @$data;
    $data = [ map { { 'name' => $_ } } sort keys %names ];
  } else {
    $data = BSXPath::match($rootnode, $match) || [];
    # epoch?
    @$data = sort {Build::Rpm::verscmp($b->{'version'}, $a->{'version'}) || $a->{'name'} cmp $b->{'name'} || $a->{'arch'} cmp $b->{'arch'}} @$data;
    delete $_->{'versrel'} for @$data;
  }
  my $res = {};
  $res->{'matches'} = @$data;
  $res->{'limited'} = 'true' if $limit && @$data > $limit;
  splice(@$data, $limit) if $limit && @$data > $limit;
  delete $_->{'path'} for @$data;
  search_add_downloadurl($data) if $cgi->{'withdownloadurl'};
  $res->{'binary'} = $data;
  return ($res, $BSXML::collection);
}

sub search_published_binary_name {
  my ($cgi, $match) = @_;
  return search_published_binary_id($cgi, $match, 1);
}

sub published_repoinfo_keys2prps {
  my (@k) = @_;
  return @k unless $BSConfig::published_db_sqlite;
  for (@k) {
    my @p = split('/', $_); 
    splice(@p, 0, 2, "$p[0]$p[1]") while @p > 1 && $p[0] =~ /:$/;
    my $projid = shift @p;
    splice(@p, 0, 2, "$p[0]$p[1]") while @p > 1 && $p[0] =~ /:$/;
    my $repoid = shift @p;
    $_ = "$projid/$repoid";
  }    
  return @k;
}

sub published_repoinfo_projectindexfunc {
  my ($db, $path, $value, $lkeys, $hint, $hintval) = @_;
  if ($BSConfig::published_db_sqlite) {
    return $db->rawkeys($path, $value) if defined $value;
    return $db->rawvalues($path, $hint, $hintval);
  }
  $db->{'_allkeys'} ||= [ $db->rawkeys() ] unless $lkeys;
  if (!defined($value)) {
    my %projids;
    $projids{(split('/', $_, 2))[0]} = 1 for @{$lkeys || $db->{'_allkeys'}};
    return sort keys %projids;
  }
  return grep {/^\Q$value\E\//} @{$lkeys || $db->{'_allkeys'}};
}

sub published_repoinfo_repositoryindexfunc {
  my ($db, $path, $value, $lkeys, $hint, $hintval) = @_;
  $db->{'_allkeys'} ||= [ $db->rawkeys() ] unless $lkeys;
  my @k = published_repoinfo_keys2prps(@{$lkeys || $db->{'_allkeys'}});
  if (!defined($value)) {
    my %repoids;
    $repoids{(split('/', $_, 2))[1]} = 1 for @k;
    return sort keys %repoids;
  }
  @k = grep {/\/\Q$value\E$/} @k;
  if ($BSConfig::published_db_sqlite) {
    s/:/:\//g for @k;
  }
  return @k;
}

sub search_published_repoinfo_id {
  my ($cgi, $match) = @_;
  my $repoinfodb;
  if ($BSConfig::published_db_sqlite) {
    $repoinfodb = BSSrcServer::SQLite::opendb($extrepodb, 'repoinfo');
  } else {
    $repoinfodb = BSDB::opendb($extrepodb, 'repoinfo');
  }
  $repoinfodb->{'allkeyspath'} = sub {die("413 refusing to get all keys\n")};
  $repoinfodb->{'indexfunc'} = {'project' => \&published_repoinfo_projectindexfunc, 'repository' => \&published_repoinfo_repositoryindexfunc };
  $repoinfodb->{'noindexatall'} = 1;
  my $limit = defined($cgi->{'limit'}) ? $cgi->{'limit'} : 1000;
  my $rootnode = BSXPathKeys::node($repoinfodb, '', $limit && $limit < 10 ? 1000 : $limit * 100);
  my $matches = $rootnode->keymatch($match) || [];
  @$matches = published_repoinfo_keys2prps(@$matches);
  for (@$matches) {
    my ($projid, $repoid) = split('/', $_, 2);
    $_ = { 'project' => $projid, 'repository' => $repoid };
    if ($cgi->{'withdownloadurl'}) {
      my $du = BSUrlmapper::get_downloadurl("$projid/$repoid");
      $_->{'downloadurl'} = $du if $du;
    }
  }
  my $res = { 'matches' => scalar(@$matches), 'repoinfo' => $matches };
  if ($limit && @$matches > $limit) {
    $res->{'limited'} = 'true';
    splice(@$matches, $limit);
  }
  return ($res, $BSXML::collection);
}

sub search_published_pattern_id {
  my ($cgi, $match) = @_;
  my $patterndb;
  if ($BSConfig::published_db_sqlite) {
    $patterndb = BSSrcServer::SQLite::opendb($extrepodb, 'pattern');
  } else {
    $patterndb = BSDB::opendb($extrepodb, 'pattern');
  }
  $patterndb->{'noindex'} = {'project' => 1, 'repository' => 1};
  $patterndb->{'indexfunc'} = {'project' => \&published_binary_projectindexfunc } if $BSConfig::published_db_sqlite;
  $patterndb->{'fetch'} = \&pattern_key_to_data;
  $patterndb->{'fetch_project'} = \&binary_key_to_project;
  $patterndb->{'fetch_repository'} = \&binary_key_to_repository;
  my $limit = defined($cgi->{'limit'}) ? $cgi->{'limit'} : 1000;
  my $rootnode = BSXPathKeys::node($patterndb, '', $limit && $limit < 10 ? 1000 : $limit * 100);
  my $data = BSXPath::match($rootnode, $match) || [];
  my $res = {};
  $res->{'matches'} = @$data;
  $res->{'limited'} = 'true' if $limit && @$data > $limit;
  splice(@$data, $limit) if $limit && @$data > $limit;
  for (@$data) {
    delete $_->{'path'};
    delete $_->{'description'};
    delete $_->{'summary'};
  }
  search_add_downloadurl($data) if $cgi->{'withdownloadurl'};
  $res->{'pattern'} = $data;
  return ($res, $BSXML::collection);
}

#############################################################################

sub search {
  my ($cgi, $in, $match) = @_;
  # gather all data
  my $data = [];
  if ($in eq 'projects') {
    for my $projid (findprojects()) {
      my $proj = BSRevision::readproj_local($projid);
      push @$data, $proj;
    }
  } elsif ($in eq 'packages') {
    for my $projid (findprojects()) {
      my @packages = BSRevision::lspackages_local($projid);
      for my $packid (@packages) {
        my ($pack, undef) = getpackage($cgi, $projid, $packid);
	$pack->{'project'} = $projid;
        push @$data, $pack;
      }
    }
  } else {
    die("'in' parameter needs to be either 'projects' or 'packages'\n");
  }
  my $res;
  if ($cgi->{'values'}) {
    $data = BSXPath::valuematch($data, $match);
    $res = {'value' => $data};
  } else {
    $data = BSXPath::match($data, $match);
    if (exists $cgi->{'return'}) {
      $data = BSXPath::valuematch($data, $cgi->{'return'});
      $res = {'value' => $data};
    } elsif ($in eq 'projects') {
      $res = {'project' => $data};
    } else {
      $res = {'package' => $data};
    }
  }
  return ($res, $BSXML::collection);
}

sub postrepo {
  my ($cgi, $projid, $repoid, $arch) = @_;
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my $param = {
    'uri' => "$reposerver/build/$projid/$repoid/$arch/_repository",
    'request' => 'POST',
  };
  my $res = BSWatcher::rpc($param, $BSXML::collection, "match=$cgi->{'match'}");
  return ($res, $BSXML::collection);
}

sub missingdodresources {
  my ($cgi, $projid, $repoid, $arch) = @_;
  checkprojrepoarch($projid, $repoid, $arch);
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my $param = {
    'uri' => "$reposerver/build/$projid/$repoid/$arch/_repository",
    'request' => 'POST',
    'ignorestatus' => 1,
    'receiver' => \&BSServer::reply_receiver,
  };
  my @args = BSRPC::args($cgi, 'resource', 'partition');
  BSWatcher::rpc($param, undef, 'cmd=missingdodresources', @args);
  return undef;
}

####################################################################

sub service {
  my ($cgi, $service) = @_;
  die("404 no such service '$service'\n") unless $BSConfig::serviceserver;
  return BSWatcher::rpc("$BSConfig::serviceserver/service/$service", undef);
}

sub listservices {
  my ($cgi) = @_;
  return "<servicelist/>\n" unless $BSConfig::serviceserver;
  return BSWatcher::rpc("$BSConfig::serviceserver/service", undef);
}

####################################################################

sub published {
  my ($cgi, $projid, $repoid, $arch, $filename, $subfilename) = @_;
  my $projpack;
  die("unknown view '$cgi->{'view'}'\n") if $cgi->{'view'} && $cgi->{'view'} ne 'ymp' && $cgi->{'view'} ne 'fileinfo';
  if (defined($projid) && defined($repoid) && $cgi->{'view'} && $cgi->{'view'} eq 'ymp') {
    # attach projpack data so that the repo server does not need to
    # reconnect us
    $projpack = (getprojpack({'nopackages' => 1, 'withrepos' => 1, 'expandedrepos' => 1, 'withconfig' => 1}, [ $projid ], [ $repoid ], undef, 'noarch'))[0];
    my $proj = $projpack->{'project'}->[0];
    die("no such project\n") unless $proj && $proj->{'name'} eq $projid;
    my $repo = $proj->{'repository'}->[0];
    die("no such repository\n") unless $repo && $repo->{'name'} eq $repoid;
    $projpack->{'project'} = [ $proj ];
    my @nprojids = grep {$_ ne $projid} map {$_->{'project'}} @{$repo->{'path'} || []};
    @nprojids = BSUtil::unify(@nprojids);
    for my $nprojid (@nprojids) {
      my $nproj = (getproject({'withconfig' => 1}, $nprojid))[0];
      push @{$projpack->{'project'}}, {
	'name' => $nprojid,
	'title' => $nproj->{'title'} || '',
	'description' => $nproj->{'description'} || '',
	'config' => $nproj->{'config'} || '',
      };
    }
  }
  my @args;
  push @args, "view=$cgi->{'view'}" if $cgi->{'view'};
  my $p = "/published";
  $p .= "/$projid" if defined $projid;
  $p .= "/$repoid" if defined $repoid;
  $p .= "/$arch" if defined $arch;
  $p .= "/$filename" if defined $filename;
  $p .= "/$subfilename" if defined $subfilename;
  if (defined($projid) || !$BSConfig::partitioning || !$BSConfig::partitionservers) {
    my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
    my $param = {
      'uri' => "$reposerver$p",
      'ignorestatus' => 1,
      'receiver' => \&BSServer::reply_receiver,
    };
    if ($projpack) {
      $param->{'request'} = 'POST';
      $param->{'data'} = BSUtil::toxml($projpack, $BSXML::projpack);
      $param->{'headers'} = [ 'Content-Type: application/octet-stream' ];
    }
    BSWatcher::rpc($param, undef, @args);
    return undef;
  }
  
  my %pubprojids;
  my @reposervers = BSSrcServer::Partition::allreposervers();
  for my $reposerver (@reposervers) {
    my $res;
    eval {
      $res = BSWatcher::rpc("$reposerver/published", $BSXML::dir, @args);
    };
    warn($@) if $@;
    next unless $res;
    $pubprojids{$_->{'name'}} = 1 for @{$res->{'entry'} || []};
  }
  my @res = sort(keys %pubprojids);
  @res = map {{'name' => $_}} @res;
  return ({'entry' => \@res}, $BSXML::dir);
}

sub published_status {
  my ($cgi, $projid, $repoid) = @_;
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my $param = {
    'uri' => "$reposerver/published/$projid/$repoid",
    'ignorestatus' => 1,
    'receiver' => \&BSServer::reply_receiver,
  };
  BSWatcher::rpc($param, undef, "view=status");
  return undef;
}

sub published_path {
  my ($cgi, $projid, $repoid) = @_;
  my $medium = $cgi->{'medium'};
  my $prp = "$projid/$repoid";
  my $ret = {'project' => $projid, 'repository' => $repoid};
  $ret->{'medium'} = $medium if $medium;

  # update to get fresh version of repodownload
  BSConfiguration::check_configuration_once();
  my ($path, $url) = BSUrlmapper::get_path_downloadurl($prp);

  if ($cgi->{'filename'}) {
    # called from getbinary
    my $bin = $cgi->{'filename'};
    my $p;
    if ($bin =~ /^.+-[^-]+-[^-]+\.([a-zA-Z][^\/\.\-]*)\.d?rpm$/) {
      $p = "$1/$bin";
    } elsif ($bin =~ /^.+_[^_]+_([^_\.]+)\.deb$/) {
      $p = "$1/$bin";
    } elsif ($bin =~ /\.exe$/) {
      $p = $bin;
    } elsif ($bin =~ /\.(xz|qcow2|vmx|box|tar\.gz)$/) {
      $p = $bin;
    } elsif ($bin =~ /\.pkg\.tar(?:\.gz|\.xz|\.zst)$/) {
      $p = ($cgi->{'arch'} eq 'i586' ? 'i686' : $cgi->{'arch'})."/$bin";
    } elsif ($bin =~ /\.iso(\.report)?$/) {
      $p = "iso/$bin";
    } elsif ($bin =~ /-Media\d+$/) {
      $medium = $bin;
    }
    if (defined($p) && !$medium) {
      $path .= "/$p" if defined $path;
      $url .= BSHTTP::urlencode($p) if defined $url;
    }
  }

  if ($medium && $medium =~ /\.iso$/) {
    $medium = "iso/$medium";
  } elsif ($medium) {
    my @path = expandsearchpath($projid, $repoid);
    my $c = concatconfigs($projid, $repoid, undef, @path);
    my $bconf = Build::read_config('noarch', [ split("\n", $c) ]);
    my %repotype;
    for (@{$bconf->{'repotype'} || []}) {
      if (/^(.*?):(.*)$/) {
        $repotype{$1} = [ split(':', $2) ];
      } else {
        $repotype{$_} = [];
      }    
    }
    if ($repotype{'slepool'}) {
      my $name = $repotype{'slepool'}->[0] || 'product';
      if ($medium =~ /-Media1$/) {
	$medium = $name;
      } elsif ($medium =~ /-Media3$/) {
	$medium = "${name}_debug";
      } elsif ($medium =~ /-Media2$/) {
	my $repoorigins;
	eval { $repoorigins = prp_to_repoorigins($prp) };
	$medium = $repoorigins->{"${name}_source"} ? "${name}_source" : "${name}_debug";
      }
    } else {
      $medium = "repo/$medium";
    }
  }
  if ($medium) {
    $path .= "/$medium" if defined $path;
    $url .= BSHTTP::urlencode($medium) if defined $url;
  }
  $url =~ s/([^\/])\/$/$1/ if defined $url;		# compat
  $ret->{'path'} = $path if defined $path;
  $ret->{'url'} = $url if defined $url;
  return ($ret, $BSXML::publishedpath);
}

sub published_collection {
  my ($cgi) = @_;
  my $projids = $cgi->{'project'};
  my $names = $cgi->{'name'};
  my $binarydb;
  if ($BSConfig::published_db_sqlite) {
    $binarydb = BSSrcServer::SQLite::opendb($extrepodb, 'binary');
  } else {
    $binarydb = BSDB::opendb($extrepodb, 'binary');
  }
  $binarydb->{'indexfunc'} = {'project' => \&published_binary_projectindexfunc };
  my @k;
  if ($names) {
    for my $name (@$names) {
      push @k, $binarydb->keys('name', $name);
    }
    @k = BSUtil::unify(sort @k);
    return ({'matches' => 0}, $BSXML::collection) unless @k;
  }
  if ($projids) {
    if (@k) {
      my %projids = map {$_ => 1} @$projids;
      @k = grep {$projids{binary_key_to_project($binarydb, $_)}} @k;
    } else {
      for my $projid (@$projids) {
	push @k, $binarydb->keys('project', $projid);
      }
      @k = BSUtil::unify(sort @k);
    }
    return ({'matches' => 0}, $BSXML::collection) unless @k;
  }
  die("need at least one filter\n") unless @k;
  for (@k) {
    $_ = binary_key_to_data($binarydb, $_);
    delete $_->{'path'};
    delete $_->{'versrel'};
  }
  return ({'binary' => \@k, 'matches' => scalar(@k)}, $BSXML::collection);
}
  
####################################################################

sub autoextend_check {
  my ($skprojid, $pk) = @_;
  return $pk unless $pk;
  my $ex = 0;
  eval { $ex = BSPGP::pk2expire(BSPGP::unarmor($pk)) };
  if ($ex && $ex < time() + 14 * 24 * 3600) {
    extendkey({'comment' => 'auto-extend public key expiry date'}, $skprojid);
    $pk = readstr("$projectsdir/$skprojid.pkg/_pubkey", 1);
  }
  return $pk;
}

sub findsignkey {
  my ($projid) = @_;
  while ($projid ne '') {
    my $sk = readstr("$projectsdir/$projid.pkg/_signkey", 1);
    return ($projid, $sk) if $sk;
    $projid =~ s/[^:]*$//;
    $projid =~ s/:$//;
  }
  return undef, undef;
}

sub getsignkey {
  my ($cgi, $projid) = @_;

  my ($skprojid, $sk) = findsignkey($projid);
  return ('', 'Content-Type: text/plain') unless $sk;
  if ($cgi->{'withpubkey'} || $cgi->{'withalgo'}) {
    my $signtype = $cgi->{'signtype'};
    my $signflavor = $cgi->{'signflavor'};
    my $pk;
    if (length($sk) <= 2 && ($signtype || $signflavor)) {
      $pk = BSSrcServer::Signkey::getdefaultpubkey($projid, $signtype, $signflavor);
    } else {
      $pk = readstr("$projectsdir/$skprojid.pkg/_pubkey", 1);
      undef $pk if $pk && length($pk) <= 2;
      $pk = autoextend_check($skprojid, $pk) if $cgi->{'withpubkey'} && $cgi->{'autoextend'};
      $pk = BSSrcServer::Signkey::getdefaultpubkey($projid, $signtype, $signflavor) if !$pk && $BSConfig::sign_project && $BSConfig::sign;
    }
    if ($cgi->{'withalgo'} && $sk !~ /^\S+:/) {
      my $algo = '?';
      eval { $algo = BSPGP::pk2algo(BSPGP::unarmor($pk)) if $pk };
      $sk = "$algo:$sk";
    }
    if ($cgi->{'withpubkey'}) {
      $sk .= "\n" unless $sk =~ /\n$/s;
      $sk .= $pk if defined $pk;
    }
  }
  return ($sk, 'Content-Type: text/plain');
}

sub projid2sslcert {
  my ($skprojid, $sk, $projid, $nocreate, $signtype, $signflavor) = @_;
  if ($BSConfig::disable_sslcert_cache && $BSConfig::disable_sslcert_cache->{$skprojid}) {
    # we ignore nocreate here for now
    return BSSrcServer::Signkey::pubkey2sslcert($projid || $skprojid, "$projectsdir/$skprojid.pkg/_pubkey", "$projectsdir/$skprojid.pkg/_signkey", $signtype, $signflavor);
  }
  my $rev = BSRevision::getrev_meta($skprojid, undef);
  my $files = BSRevision::lsrev($rev);
  if (!$files->{'_sslcert'}) {
    if (!$sk || length($sk) <= 2) {
      # length(signkey) <= 2 means special handling: do not commit it
      return BSSrcServer::Signkey::pubkey2sslcert($projid || $skprojid, "$projectsdir/$skprojid.pkg/_pubkey", "$projectsdir/$skprojid.pkg/_signkey", $signtype, $signflavor);
    }
    return undef if $nocreate;
    my $cert = BSSrcServer::Signkey::pubkey2sslcert($skprojid, "$projectsdir/$skprojid.pkg/_pubkey", "$projectsdir/$skprojid.pkg/_signkey");
    mkdir_p($uploaddir);
    writestr("$uploaddir/sslcert.$$", undef, $cert);
    BSRevision::addrev_meta_replace({'comment' => 'automatic cert creation'}, $skprojid, undef, [ "$uploaddir/sslcert.$$", undef, '_sslcert']);
    return $cert;
  }
  return BSRevision::revreadstr($rev, '_sslcert', $files->{'_sslcert'});
}

sub getsslcert {
  my ($cgi, $projid) = @_;
  my ($skprojid, $sk) = findsignkey($projid);
  my $signtype = $cgi->{'signtype'};
  my $signflavor = $cgi->{'signflavor'};
  if ($sk) {
    my $pk = readstr("$projectsdir/$skprojid.pkg/_pubkey", 1);
    $pk = autoextend_check($skprojid, $pk) if $cgi->{'autoextend'};
    my $cert = projid2sslcert($skprojid, $sk, $projid, 0, $signtype, $signflavor);
    return ($cert, 'Content-Type: text/plain');
  }
  my $cert;
  if ($BSConfig::sign_project && $BSConfig::sign) {
    $cert = BSSrcServer::Signkey::getdefaultcert($projid, $signtype, $signflavor);
  } elsif ($BSConfig::certfile) {
    $cert = readstr($BSConfig::certfile, 1);
  }
  return ($cert || '', 'Content-Type: text/plain');
}

sub getkeyinfo {
  my ($cgi, $projid) = @_;

  my $proj = checkprojrepoarch($projid, undef, undef, 1);
  if ($proj->{'remoteurl'}) {
    my $param = {
      'uri' => "$proj->{'remoteurl'}/source/$proj->{'remoteproject'}/_keyinfo",
      'timeout' => 600,
      'proxy' => $proxy,
    };
    my @args = BSRPC::args($cgi, 'withsslcert', 'autoextend', 'donotcreatecert', 'signtype');
    my $keyinfo = BSRPC::rpc($param, undef, @args);
    $keyinfo->{'project'} = BSSrcServer::Remote::maptoremote($proj, $keyinfo->{'project'}) if $keyinfo->{'project'};
    return ($keyinfo, $BSXML::keyinfo);
  }
  my ($skprojid, $sk) = findsignkey($projid);
  my $signtype = $cgi->{'signtype'};
  my $signflavor = $cgi->{'signflavor'};
  my ($pk, $cert);
  if ($sk) {
    $pk = readstr("$projectsdir/$skprojid.pkg/_pubkey", 1);
    undef $pk if $pk && length($pk) <= 2;
    $pk = autoextend_check($skprojid, $pk) if $cgi->{'autoextend'};
  }
  if ($cgi->{'withsslcert'} && $skprojid && $sk) {
    $cert = projid2sslcert($skprojid, $sk, $projid, $cgi->{'donotcreatecert'}, $signtype, $signflavor);
  }
  if ($cgi->{'withsslcert'} && !$skprojid && $BSConfig::sign_project && $BSConfig::sign) {
    if ($cgi->{'donotcreatecert'}) {
      eval { $cert = BSSrcServer::Signkey::getdefaultcert($projid, $signtype, $signflavor) };
    } else {
      $cert = BSSrcServer::Signkey::getdefaultcert($projid, $signtype, $signflavor);
    }
  } elsif ($cgi->{'withsslcert'} && !$skprojid && $BSConfig::certfile) {
    $cert = readstr($BSConfig::certfile, 1);
  }
  if (!$pk && $BSConfig::sign_project && $BSConfig::sign) {
    $pk = BSSrcServer::Signkey::getdefaultpubkey($projid, $signtype, $signflavor);
  } elsif (!$pk && $BSConfig::keyfile) {
    $pk = readstr($BSConfig::keyfile, 1);
  }
  my $keyinfo = {};
  $keyinfo->{'project'} = $skprojid if $skprojid;
  if ($pk) {
    $keyinfo->{'pubkey'} = BSSrcServer::Signkey::pubkeyinfo($pk);
    $keyinfo->{'pubkey'}->{'_content'} = $pk;
  }
  if ($cert) {
    $keyinfo->{'sslcert'} = BSSrcServer::Signkey::certinfo($cert);
    $keyinfo->{'sslcert'}->{'_content'} = $cert;
  }
  return ($keyinfo, $BSXML::keyinfo);
}

####################################################################

# next two functions needed for migrations to 2.4
sub getlastidrequest {
  my $lastid = (readstr("$requestsdir/.nextid", 1) || 0) - 1;
  return ("$lastid\n", 'Content-Type: text/plain');
}

sub getrequest {
  my ($cgi, $id) = @_;
  local *F;
  if (!open(F, '<', "$requestsdir/$id") && !open(F, '<', "$oldrequestsdir/$id")) {
    die("404 no such request '$id'\n");
  }
  my $reqxml = '';
  1 while sysread(F, $reqxml, 8192, length($reqxml));
  close F;
  return ($reqxml, 'Content-Type: text/xml');
}

####################################################################

sub worker_findremote {
  my ($projid) = @_;

  my $proj = BSRevision::readproj_local($projid, 1);
  $proj = BSSrcServer::Remote::remoteprojid($projid) if !$proj || $proj->{'remoteurl'};
  die("404 project '$projid' does not exist\n") unless $proj;
  if (!$proj->{'remoteurl'}) {
    $proj->{'remoteurl'} = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid, 1) : ($BSConfig::workerreposerver || $BSConfig::reposerver);
    $proj->{'remoteproject'} = $projid;
    $proj->{'remoteproxy'} = undef;
  }
  return $proj;
}

sub worker_getbinaries {
  my ($cgi, $projid, $repoid, $arch) = @_;

  if (!$BSStdServer::isajax) {
    my @args;
    push @args, "project=$projid";
    push @args, "repository=$repoid";
    push @args, "arch=$arch";
    push @args, "binaries=$cgi->{'binaries'}";
    push @args, map {"module=$_"} @{$cgi->{'module'} || []};
    push @args, "now=$cgi->{'now'}" if $cgi->{'now'};
    BSHandoff::handoff_part('interconnect_out', '/getbinaries', undef, @args);
  }
  my @binaries = split(',', $cgi->{'binaries'});
  my $proj = worker_findremote($projid);
  my $binarylist = BSSrcServer::Remote::getremotebinarylist($proj, $projid, $repoid, $arch, \@binaries, $cgi->{'module'});
  return undef unless $binarylist;
  my $reply = BSSrcServer::Remote::getremotebinaries($proj, $projid, $repoid, $arch, \@binaries, $binarylist, $cgi->{'module'});
  return undef unless $reply;
  if ($cgi->{'raw'}) {
    die("can only transport one binary in raw mode\n") unless @$reply == 1;
    my $f = $reply->[0];
    die("$f->{'name'}: $f->{'error'}\n") if $f->{'error'};
    die("$f->{'name'}: not found\n") unless $f->{'filename'};
    BSWatcher::reply_file($f->{'filename'});
    return undef;
  }
  BSWatcher::reply_cpio($reply);
  return undef;
}

sub worker_getbinaryversions {
  my ($cgi, $projid, $repoid, $arch) = @_;

  if (!$BSStdServer::isajax) {
    my @args;
    push @args, "project=$projid";
    push @args, "repository=$repoid";
    push @args, "arch=$arch";
    push @args, "binaries=$cgi->{'binaries'}";
    push @args, BSRPC::args($cgi, 'nometa', 'module', 'now', 'withevr');
    BSHandoff::handoff_part('interconnect_out', '/getbinaryversions', undef, @args);
  }
  my @binaries = split(',', $cgi->{'binaries'});
  my $proj = worker_findremote($projid);
  my $binaryversions = BSSrcServer::Remote::getremotebinaryversions($proj, $projid, $repoid, $arch, \@binaries, $cgi->{'module'}, $cgi->{'withevr'});
  return undef unless $binaryversions;
  my $bvl = {};
  $bvl->{'binary'} = [ map {$binaryversions->{$_}} @binaries];
  return ($bvl, $BSXML::binaryversionlist);
}

sub getobsgendiffdata {
  my ($cgi, $projid, $repoid, $arch) = @_;
  checkprojrepoarch($projid, $repoid, $arch);
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid, 1) : ($BSConfig::workerreposerver || $BSConfig::reposerver);
  my $param = {
    'uri' => "$reposerver/getobsgendiffdata",
    'ignorestatus' => 1,
    'receiver' => \&BSServer::reply_receiver,
  };
  BSWatcher::rpc($param, undef, BSRPC::args($cgi, 'project', 'repository', 'arch', 'jobid', 'workerid'));
  return undef;
}

sub getmodulemd {
  my ($cgi, $projid, $packid, $srcmd5) = @_;
  my $rev = {'project' => $projid, 'package' => $packid, 'srcmd5' => $srcmd5};
  my $files = BSRevision::lsrev($rev);
  die("no modulemd data in $projid/$packid\n") unless $files->{'_modulemd.yaml'};
  my @s = BSRevision::revstat($rev, '_modulemd.yaml', $files->{'_modulemd.yaml'});
  die("_modulemd.yaml: maximum size exceeded\n") if $s[7] > 1024*1204;
  my $yaml = BSRevision::revreadstr($rev, '_modulemd.yaml', $files->{'_modulemd.yaml'});
  my $mds = BSSrcServer::Modulemd::read_modulemds($yaml);
  for my $md (@$mds) {
    next unless $md->{'document'} eq 'modulemd';
    BSSrcServer::Modulemd::tostream($md, $cgi->{'module'} || [], $cgi->{'modularitylabel'}, $cgi->{'modularityplatform'});
    $md->{'data'}->{'arch'} = $cgi->{'arch'} if $cgi->{'arch'};
  }
  my $view = $cgi->{'view'} || 'storable';
  if ($view eq 'yaml') {
    require Build::Modulemd;
    my $yaml = '';
    $yaml .= Build::Modulemd::mdtoyaml($_) for @$mds;
    return ($yaml, 'Content-Type: application/octet-stream');
  } elsif ($view eq 'storable') {
    return (BSUtil::tostorable($mds), 'Content-Type: application/octet-stream');
  } else {
    die("unknown view '$view'\n");
  }
}

####################################################################

# this is shared for AJAX requests
my @lastev_cache;
my @lastev_stat;

sub filterlastevents {
  my ($events, $filter) = @_;
  my %filter = map {$_ => 1} @$filter;
  my @ev;
  for my $ev (@$events) {
    if ($ev->{'type'} eq 'package') {
      next unless defined $ev->{'package'};
      next unless $filter{"package/$ev->{'project'}/$ev->{'package'}"} || $filter{"package/$ev->{'project'}"};
    } elsif ($ev->{'type'} eq 'project') {
      next unless $filter{"project/$ev->{'project'}"};
    } elsif ($ev->{'type'} eq 'repository' || $ev->{'type'} eq 'repoinfo') {
      next unless $filter{"repository/$ev->{'project'}/$ev->{'repository'}/$ev->{'arch'}"} || $filter{"repository/$ev->{'project'}/$ev->{'repository'}"};
    } else {
      next;
    }
    push @ev, $ev;
  }
  return @ev;
}

sub lastevents {
  my ($cgi, $filter) = @_;
  if (!$cgi->{'start'}) {
    # just fetch the current event number
    my $lastev = BSFileDB::fdb_getlast("$eventdir/lastevents", $eventlay);
    my $lastno = $lastev ? $lastev->{'number'} : 0;
    my $ret = {'next' => $lastno, 'sync' => 'lost'};
    return ($ret, $BSXML::events);
  }
  my $block = !exists($cgi->{'block'}) || $cgi->{'block'};
  if (!$BSStdServer::isajax && $block) {
    my @args;
    push @args, "client=$cgi->{'client'}" if $cgi->{'client'};
    push @args, "obsname=$cgi->{'obsname'}" if $cgi->{'obsname'};
    push @args, map {"filter=$_"} @{$filter || []};
    push @args, "start=$cgi->{'start'}";
    if ($cgi->{'client'}) {
      BSHandoff::handoff_part('lastevents', '/lastevents', undef, @args);
    } else {
      BSHandoff::handoff_part('interconnect_in_lastevents', '/lastevents', undef, @args);
    }
  }
  BSWatcher::addfilewatcher("$eventdir/lastevents", 120) if $BSStdServer::isajax;

  my @s = stat("$eventdir/lastevents");
  my @events;
  my ($firstno, $nextno);
  if (@s && @lastev_stat && "$s[9]/$s[7]/$s[1]" eq "$lastev_stat[9]/$lastev_stat[7]/$lastev_stat[1]") {
    @events = @lastev_cache;
  } else {
    my $lastev = BSFileDB::fdb_getlast("$eventdir/lastevents", $eventlay);
    push @events, $lastev if $lastev;
    @lastev_cache = @events;
    @lastev_stat = @s;
  }
  $firstno = @events ? $events[0]->{'number'} : 0;
  $nextno = @events ? $events[-1]->{'number'} + 1 : 1;

  if ($cgi->{'start'} < $firstno) {
    # get last 5
    @events = BSFileDB::fdb_getall_reverse("$eventdir/lastevents", $eventlay, 5);
    @events = reverse @events;
    @lastev_cache = @events;
    @lastev_stat = @s;
    $firstno = @events ? $events[0]->{'number'} : 0;
    $nextno = @events ? $events[-1]->{'number'} + 1 : 1;
  }

  if ($cgi->{'start'} < $firstno) {
    my $cnt = $nextno - $cgi->{'start'};
    if ($cnt > 5) {
      @events = BSFileDB::fdb_getall_reverse("$eventdir/lastevents", $eventlay, $cnt);
      @events = reverse @events;
      if (@events < 20) {
        @lastev_cache = @events;
        @lastev_stat = @s;
      }
      $firstno = @events ? $events[0]->{'number'} : 0;
      $nextno = @events ? $events[-1]->{'number'} + 1 : 1;
    }
  }

  if ($cgi->{'start'} < $firstno) {
    # we have to get them all
    @events = BSFileDB::fdb_getall("$eventdir/lastevents", $eventlay);
    # re-calculate in case something has changed
    $firstno = @events ? $events[0]->{'number'} : 0;
    $nextno = @events ? $events[-1]->{'number'} + 1 : 1;
    if ($firstno > $cgi->{'start'}) {
      # out of sync!
      return ({'next' => $nextno, 'sync' => 'lost'}, $BSXML::events);
    }
  }

  # filter
  @events = grep {$_->{'number'} >= $cgi->{'start'}} @events;
  @events = filterlastevents(\@events, $filter) if $filter && @events;

  # return a sync reply every 100 events / 5 minutes for two reasons
  # - get rid of old peers
  # - survive history truncation
  $cgi->{'start_orig'} ||= $cgi->{'start'};
  $cgi->{'req_time'} ||= time();
  if ($block && $BSStdServer::isajax && !@events && $nextno < $cgi->{'start_orig'} + 100 && time() < $cgi->{'req_time'} + 300) {
    # small hack: update cgi to the next event number
    $cgi->{'start'} = $nextno if $cgi->{'start'} < $nextno;
    return undef;
  }
  for (@events) {
    $_ = { %$_ };	# clone em
    # delete unwanted fields
    delete $_->{'time'};
    delete $_->{'number'};
    # clean up a bit
    delete $_->{'package'} unless defined($_->{'package'}) && $_->{'package'} ne '';
  }
  my $ret = {'next' => $nextno};
  $ret->{'event'} = \@events if @events;
  return ($ret, $BSXML::events);
}

sub prunelastevents {
  my ($sizemin, $sizemax, $cutmin, $cutmax) = @_;
  local *F;
  BSUtil::lockopen(\*F, '+>>', "$eventdir/lastevents");
  defined(sysseek(\*F, 0, 0)) || die("sysseek: $!\n");
  my $events = '';

  # read first 32 bytes to get (number, time)
  my $l = sysread(\*F, $events, 32);
  my @s = split('\|', $events, 3);
  if (@s > 2 && $s[1] && $s[1] > $cutmax) {
    close(F);
    return;
  }

  # read rest of the file in 8k blocks
  sysread(\*F, $events, 8192 - 32, length($events));
  1 while sysread(\*F, $events, 8192, length($events));

  if (length($events) < $sizemax) {
    close(F);
    return;
  }

  # fast pruning by size
  my $pi = index($events, "\n", length($events) - $sizemin);
  if ($pi >= 0) {
    my @s = split('\|', substr($events, $pi + 1, 32), 3);
    substr($events, 0, $pi + 1, '') if @s > 2 && $s[1] && $s[1] <= $cutmin;
  }

  # prune to sizemin or cutmin
  while (length($events) >= $sizemin) {
    my @s = split('\|', substr($events, 0, 32), 3);
    last if @s > 2 && $s[1] && $s[1] > $cutmin;
    my $i = index($events, "\n");
    last unless $i >= 0;
    substr($events, 0, $i + 1, '');
  }

  writestr("$eventdir/.lastevents", "$eventdir/lastevents", $events) if $events;
  close(F);
}

#
# add an event to the "lastevents" queue used in the build service
# interconnect implementation
#
sub addevent {
  my ($ev) = @_;

  # check the "access" flag. if the project has access turned
  # off, do not add it to lastevents.
  # XXX: maybe better to add a "noaccess" marker to the event
  # and filter in the request
  if (defined($ev->{'project'})) {
    my $proj = BSRevision::readproj_local($ev->{'project'}, 1);
    my $access = 1;
    $access = BSUtil::enabled('', $proj->{'access'}, $access, '') if $proj && $proj->{'access'};
    # XXX: may also check packages in the future
    return unless $access;
  }
  my $now = time();
  $ev->{'time'} = $now;
  mkdir_p($eventdir);
  my $sizemin = 262144;		# keep at least 256k of data
  my $sizemax = $sizemin * 2;	# start pruning at this size
  my $timemin = 1800;		# keep at least 1/2 hour
  my $timemax = 3600;		# start pruning if first entry is older than 1 hour
  prunelastevents($sizemin, $sizemax, $now - $timemin, $now - $timemax) if -s "$eventdir/lastevents" && -s _ >= $sizemax;
  BSFileDB::fdb_add_i("$eventdir/lastevents", $eventlay, $ev);
}

sub newevent {
  my ($cgi) = @_;
  my $ev = {};
  for ('type', 'project', 'package', 'repository', 'arch', 'job') {
    $ev->{$_} = $cgi->{$_} if defined $cgi->{$_};
  }
  addevent($ev);
  return $BSStdServer::return_ok;
}

####################################################################

my %lasteventsproxy_filters;
my %lasteventsproxy_start;

sub lasteventsproxy {
  my ($cgi, $filter) = @_;
  if (!$BSStdServer::isajax) {
    my @args = BSRPC::args($cgi, 'client', 'start', 'remoteurl', 'filter');
    BSHandoff::handoff_part('interconnect_out_lastevents', '/lasteventsproxy', undef, @args);
  }
  my $remoteurl = $cgi->{'remoteurl'};
  my $start = $cgi->{'start'};
  if (!$start) {
    # just do the rpc
    my $param = {
      'uri' => "$remoteurl/lastevents",
      'request' => 'POST',
      'formurlencode' => 1,
      'proxy' => $proxy,
    };
    my @args = BSRPC::args($cgi, 'filter');
    unshift @args, "obsname=$BSConfig::obsname" if $BSConfig::obsname;
    return BSWatcher::rpc($param, undef, @args);
  }
  my $now = time();
  $cgi->{'start_orig'} ||= $cgi->{'start'};
  $cgi->{'req_time'} ||= $now;
  my $fil = $lasteventsproxy_filters{$remoteurl};
  $fil = $lasteventsproxy_filters{$remoteurl} = {} unless $fil;
  if ($filter) {
    $fil->{$_} = $now for @$filter;
  } else {
    $fil->{'*'} = $now;
  }

  # prune away old entries
  my $thres = $now - 1000;
  if (grep {$_ < $thres} values %$fil) {
    delete $fil->{$_} for grep {$fil->{$_} < $thres} keys %$fil;
  }

  my @args;
  push @args, "obsname=$BSConfig::obsname" if $BSConfig::obsname;
  push @args, "start=$start";
  push @args, map {"filter=$_"} sort keys %$fil unless $fil->{'*'};
  my $running = join(',', @args);

  # get rid of old job if it no longer matches
  my $jev = $BSServerEvents::gev;
  BSWatcher::rpc_deljob($jev) if $jev->{'lasteventsproxy_running'} && $jev->{'lasteventsproxy_running'} ne $running;

  $lasteventsproxy_start{$remoteurl} = $start if !$lasteventsproxy_start{$remoteurl} || $start > $lasteventsproxy_start{$remoteurl};
  $jev->{'lasteventsproxy_running'} = $running;
  my $param = {
    'uri' => "$remoteurl/lastevents",
    'request' => 'POST',
    'formurlencode' => 1,
    'joinable' => 1,
    'proxy' => $proxy,
  };
  my $eventlist = BSWatcher::rpc($param, $BSXML::events, @args);
  return undef unless defined $eventlist;
  delete $jev->{'lasteventsproxy_running'};

  # filter
  my @events = @{delete($eventlist->{'event'}) || []};
  @events = filterlastevents(\@events, $filter) if $filter && @events;

  my $nextno = $eventlist->{'next'};
  $lasteventsproxy_start{$remoteurl} = $nextno if $nextno && $nextno < $lasteventsproxy_start{$remoteurl};

  if (!@events && !$eventlist->{'sync'} && $nextno && $cgi->{'start'} < $nextno) {
    # magic sync code
    my $syncnextno = $lasteventsproxy_start{$remoteurl};
    $eventlist->{'next'} = $nextno = $syncnextno if $syncnextno > $cgi->{'start'} && $syncnextno < $nextno;

    # return a sync reply every 100 events / 5 minutes like in getlastevents
    if ($nextno < $cgi->{'start_orig'} + 100 && $now < $cgi->{'req_time'} + 300) {
      $cgi->{'start'} = $nextno;	# update start value
      return lasteventsproxy($cgi, $cgi->{'filter'});
    }
  }

  $eventlist->{'event'} = \@events if @events;
  return ($eventlist, $BSXML::events);
}

####################################################################

sub getrelsync {
  my ($cgi, $projid, $repoid, $arch) = @_;
  checkprojrepoarch($projid, $repoid, $arch);
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my $param = {
    'uri' => "$reposerver/build/$projid/$repoid/$arch/_relsync",
    'ignorestatus' => 1,
    'receiver' => \&BSServer::reply_receiver,
  };
  BSWatcher::rpc($param, undef);
  return undef;
}

sub postrelsync {
  my ($cgi, $projid, $repoid, $arch) = @_;

  my $proj = checkprojrepoarch($projid, $repoid, $arch);
  my $repo = (grep {$_->{'name'} eq $repoid} @{$proj->{'repository'} || []})[0];
  my $relsyncdata = BSServer::read_data(10000000);
  for my $a (@{$repo->{'arch'} || []}) {
    next if $a eq $arch;
    next if $BSConfig::relsync_pool && ($BSConfig::relsync_pool->{$arch} || '') ne ($BSConfig::relsync_pool->{$a} || '');
    my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
    my $param = {
      'uri' => "$reposerver/build/$projid/$repoid/$a/_relsync",
      'request' => 'POST',
      'data' => $relsyncdata,
    };
    eval {
      BSRPC::rpc($param);
    };
    if ($@) {
      warn($@);
    }
  }
  return $BSStdServer::return_ok;
}

####################################################################

# XXX: support multiple dispatchers
sub putdispatchprios {
  my ($cgi) = @_;
  my $dispatcher = $BSConfig::masterdispatcher || $BSConfig::reposerver;
  my $param = {
    'uri' => "$dispatcher/build/_dispatchprios",
    'request' => 'PUT',
    'data' => \&BSServer::forward_sender,
    'chunked' => 1,
  };
  return BSWatcher::rpc($param, undef);
}

sub getdispatchprios {
  my ($cgi) = @_;
  my $dispatcher = $BSConfig::masterdispatcher || $BSConfig::reposerver;
  my $param = {
    'uri' => "$dispatcher/build/_dispatchprios",
    'ignorestatus' => 1,
    'receiver' => \&BSServer::reply_receiver,
  };
  BSWatcher::rpc($param, undef);
  return undef;
}

####################################################################

sub sourceinfo_findrecipe {
  my ($rev, $repoid, $type, $files) = @_;
  return '_aggregate' if $files->{'_aggregate'};
  return '_patchinfo' if $files->{'_patchinfo'};
  if ($type) {
    return findfile($rev, $repoid, $type, $files);
  } else {
    for my $t ('spec', 'dsc', 'kiwi', 'docker', 'productcompose') {
      my $file = findfile($rev, $repoid, $t, $files);
      next unless defined $file;
      return $file;
    }
    return undef;
  }
}

sub sourceinfo_parse {
  my ($r, $projid, $packid, $bconf, $recipe, $recipefn, $servicefn) = @_;
  my $buildtype = Build::recipe2buildtype($recipe);
  if (!$buildtype) {
    $r->{'error'} = "don't know how to build $recipe";
    return;
  }
  local $bconf->{'obspackage'} = $packid;
  local $bconf->{'buildflavor'};
  if ($packid =~ /(?<!^_product)(?<!^_patchinfo):./) {
    $packid =~ /^(.*):(.*?)$/;
    $bconf->{'obspackage'} = $1;
    $bconf->{'buildflavor'} = $2;
  }
  my $d = eval { Build::parse_typed($bconf, $recipefn, $buildtype) };
  if ($@) {
    $r->{'error'} = "parse error: $@";
    $r->{'error'} =~ s/\n.*//s;
  } else {
    $d ||= { 'error' => 'parse error' };
    for (qw{name version release subpacks deps prereqs exclarch badarch error}) {
      $r->{$_} = $d->{$_} if defined $d->{$_};
    }
  }
  if ($servicefn) {
    my $services = readxml($servicefn, $BSXML::services, 1) || {};
    for my $service (@{$services->{'service'} || []}) {
      push @{$r->{'buildtimeservice'}}, $service->{'name'} if ($service->{'mode'} || '') eq 'buildtime';
    }
  }
}

sub sourceinfo {
  my ($cgi, $projid, $packid, $bconf, $ispkgsourceinfo) = @_;
  my $r = {'package' => $packid};
  my $linked = [];
  my $rev;
  my $files;
  if ($cgi->{'withmetamd5'} && $packid !~ /(?<!^_product)(?<!^_patchinfo):./) {
    my $meta = readstr("$projectsdir/$projid.pkg/$packid.xml", 1) || '';
    $r->{'metamd5'} = Digest::MD5::md5_hex($meta);
  }
  if ($ispkgsourceinfo) {
    # do the getrev outside of the eval to get better error reporting
    $rev = getrev($projid, $packid, defined($cgi->{'rev'}) ? $cgi->{'rev'} : 'build', $linked);
  }
  eval {
    $rev ||= getrev($projid, $packid, defined($cgi->{'rev'}) ? $cgi->{'rev'} : 'build', $linked);
    $r->{'srcmd5'} = $rev->{'srcmd5'} if $rev->{'srcmd5'} ne 'empty';
    $r->{'rev'} = $rev->{'rev'} if $rev->{'rev'};
    $r->{'vrev'} = $rev->{'vrev'} if $rev->{'vrev'};
    if (!$rev || $rev->{'srcmd5'} eq 'empty' || $rev->{'srcmd5'} eq $BSSrcrep::emptysrcmd5) {
      die("no source uploaded\n") unless $cgi->{'nofilename'};
      $rev = {'srcmd5' => $BSSrcrep::emptysrcmd5, 'project' => $projid, 'package' => $packid };
    }
    if (!$cgi->{'noexpand'} && $packid ne '_project') {
      my $linkinfo = {};
      $files = BSRevision::lsrev($rev, $linkinfo);
      if ($linkinfo->{'xservicemd5'}) {
	$files = BSSrcServer::Service::handleservice($rev, $files, $linkinfo->{'xservicemd5'});
	$r->{'srcmd5'} = $rev->{'srcmd5'};
      }
      my $meta = '';
      $meta .= "$files->{$_}  $_\n" for sort keys %$files;
      $r->{'verifymd5'} = Digest::MD5::md5_hex($meta);
      die("source update running\n") if $files->{'_service'} && -e "$eventdir/service/${projid}::$packid";
      die("source update failed\n") if $files->{'_service_error'};
    }
  };
  $r->{'originproject'} = $rev->{'originproject'} if $rev && $rev->{'originproject'};
  $r->{'originpackage'} = $rev->{'package'} if $rev && $rev->{'originpackage'};
  $r->{'linked'} = $linked if @$linked;
  if ($@) {
    $r->{'error'} = $@;
    $r->{'error'} =~ s/\n$//s;
    return $r;
  }
  return $r if $cgi->{'noexpand'} || $packid eq '_project';
  if ($files->{'_link'}) {
    $rev->{'linkrev'} = $cgi->{'linkrev'} if $cgi->{'linkrev'};
    eval {
      $files = BSSrcServer::Link::handlelinks($rev, $files, {'linked' => $linked});
    };
    if ($@) {
      $files = "$@";
      $files =~ s/\n$//;
    }
    $r->{'linked'} = $linked if @$linked;
    $r->{'vrev'} = $rev->{'vrev'} if $rev->{'vrev'};
    if (!ref $files) {
      $r->{'error'} = $files || 'could not get file list';
      return $r;
    }
    $r->{'lsrcmd5'} = $r->{'srcmd5'};
    $r->{'srcmd5'} = $rev->{'srcmd5'};
    my $meta = '';
    $meta .= "$files->{$_}  $_\n" for sort keys %$files;
    $r->{'verifymd5'} = Digest::MD5::md5_hex($meta);
  }
  if ($cgi->{'withchangesmd5'}) {
    $r->{'revtime'} = $rev->{'time'};
    $r->{'changesmd5'} = $files->{"$packid.changes"} if $files->{"$packid.changes"};
  }
  if (!$cgi->{'rev'} && !$cgi->{'linkrev'}) {
    BSSrcServer::Multibuild::updatemultibuild($projid, $packid, $files);
    if ($rev->{'originpackage'}) {
      my $mb = BSSrcServer::Multibuild::getmultibuild($projid, $rev->{'package'}) || {};
      if (!grep {"$rev->{'package'}:$_" eq $packid} @{$mb->{'flavor'} || $mb->{'package'} || []}) {
        $r->{'error'} = "package '$packid' does not exist";
        return $r;
      }
    }
  }
  return $r if $cgi->{'nofilename'} || $packid eq '_pattern';
  my $type = $bconf->{'type'} && $bconf->{'type'} ne 'UNDEFINED' ? $bconf->{'type'} : undef;
  my $recipe = sourceinfo_findrecipe($rev, $cgi->{'repository'}, $type, $files);
  if (!$recipe) {
    $r->{'error'} = $type ? "no file found for build type '$type'" : 'bad build configuration, no build type defined or detected';
    return $r;
  }
  $r->{'filename'} = $recipe;
  if ($cgi->{'parse'} && $recipe ne '_aggregate' && $recipe ne '_patchinfo') {
    local $Build::Rpm::includecallback = sub { rpm_includecallback($rev, $files, @_) };
    my $recipefile = BSRevision::revfilename($rev, $recipe, $files->{$recipe});
    my $servicefile = $files->{'_service'} ? BSRevision::revfilename($rev, '_service', $files->{'_service'}) : undef;
    sourceinfo_parse($r, $projid, $packid, $bconf, $recipe, $recipefile, $servicefile);
  }
  return $r;
}

sub getprojectsourceinfo {
  my ($cgi, $projid) = @_;
  my $proj = checkprojrepoarch($projid, $cgi->{'repository'}, $cgi->{'arch'}, 1);
  my @packages = @{$cgi->{'package'} || []};
  BSSrcServer::Projlink::enable_frozenlinks_cache();
  BSRevision::readproj_local_use_cache(1);
  @packages = findpackages($projid, $proj, undef, undef, $cgi->{'noexpand'}) unless @packages;
  my $bconf;
  if (!$cgi->{'nofilename'}) {
    if (!$cgi->{'repository'}) {
      my $cfile;
      $cfile = "$projectsdir/$projid.conf" if -e "$projectsdir/$projid.conf";
      $bconf = Build::read_config($cgi->{'arch'} || 'noarch', $cfile);
    } else {
      my @path = expandsearchpath($projid, $cgi->{'repository'});
      my $c = concatconfigs($projid, $cgi->{'repository'}, undef, @path);
      $bconf = Build::read_config($cgi->{'arch'} || 'noarch', [ split("\n", $c) ]);
    }
  }
  my @res;
  if (@packages > 1) {
    $BSSrcServer::Remote::collect_remote_getrev = 1;
    for my $packid (splice @packages) {
      my $r = sourceinfo($cgi, $projid, $packid, $bconf);
      if ($r->{'error'} && $r->{'error'} =~ /collect_remote_getrev$/) {
	push @packages, $packid;
	next;
      }
      push @res, $r;
    }
    $BSSrcServer::Remote::collect_remote_getrev = 0;
    BSSrcServer::Remote::fill_remote_getrev_cache();
  }
  for my $packid (@packages) {
    push @res, sourceinfo($cgi, $projid, $packid, $bconf);
  }
  BSRevision::readproj_local_use_cache(0);
  BSSrcServer::Projlink::disable_frozenlinks_cache();
  return ({'sourceinfo' => \@res}, $BSXML::sourceinfolist);
}

sub getpackagesourceinfo {
  my ($cgi, $projid, $packid) = @_;
  checkprojrepoarch($projid, $cgi->{'repository'}, $cgi->{'arch'}, 1); #remoteok
  my $bconf;
  if (!$cgi->{'nofilename'}) {
    my $cfile;
    if (!$cgi->{'repository'}) {
      $cfile = "$projectsdir/$projid.conf" if -e "$projectsdir/$projid.conf";
    } else {
      print "expandsearchpath $projid $cgi->{'repository'}...\n";
      my @path = expandsearchpath($projid, $cgi->{'repository'});
      my $c = concatconfigs($projid, $cgi->{'repository'}, undef, @path);
      $cfile = [ split("\n", $c) ];
    }
    $bconf = Build::read_config($cgi->{'arch'} || 'noarch', $cfile);
  }
  my $res = sourceinfo($cgi, $projid, $packid, $bconf, 1);
  return ($res, $BSXML::sourceinfo);
}

# callback for the recipe parser
sub getpackagesourceinfo_post_includecallback {
  my ($files, $fn) = @_;
  my %files = %$files;
  $fn =~ s/.*\///;
  if ($fn ne '_service' && $files{'_service'}) {
    for (sort keys %files) {
      next unless /^_service:.*:(.*?)$/s;
      $files{$1} = delete($files{$_}) if $files{$_};
    }
  }
  $fn = $files{$fn};
  return undef unless $fn;
  my @s = stat($fn);
  return undef if !@s || $s[7] > 100000;
  return readstr($fn);
}

sub getpackagesourceinfo_post_common {
  my ($cgi, $projid, $packid, $recipe, $buildtype, $files) = @_;
  $files ||= {};
  die("file '$recipe' does not exist\n") if $recipe && !$files->{$recipe};
  checkprojrepoarch($projid, $cgi->{'repository'}, $cgi->{'arch'}, 1);
  my $cfile;
  if (!$cgi->{'repository'}) {
    $cfile = "$projectsdir/$projid.conf" if -e "$projectsdir/$projid.conf";
  } else {
    print "expandsearchpath $projid $cgi->{'repository'}...\n";
    my @path = expandsearchpath($projid, $cgi->{'repository'});
    my $c = concatconfigs($projid, $cgi->{'repository'}, undef, @path);
    $cfile = [ split("\n", $c) ];
  }
  my $bconf = Build::read_config($cgi->{'arch'} || 'noarch', $cfile);
  $bconf->{'type'} = $buildtype if $buildtype;
  my $r = {'package' => $packid};
  if ($packid =~ /(?<!^_product)(?<!^_patchinfo):./) {
    $packid =~ /^(.*):(.*?)$/;
    $r->{'originpackage'} = $1;
  }
  return ($r, $BSXML::sourceinfo) if $packid eq '_pattern';
  if (!$recipe) {
    # create fake rev for findfile()
    my $rev = { 'package' => $packid };
    $rev = { 'package' => $r->{'originpackage'}, 'originpackage' => $packid } if $r->{'originpackage'};
    my $type = $bconf->{'type'} && $bconf->{'type'} ne 'UNDEFINED' ? $bconf->{'type'} : undef;
    $recipe = sourceinfo_findrecipe($rev, $cgi->{'repository'}, $type, $files);
    if (!$recipe) {
      $r->{'error'} = $type ? "no file found for build type '$type'" : 'bad build configuration, no build type defined or detected';
      return ($r, $BSXML::sourceinfo);
    }
  }
  $r->{'filename'} = $recipe;
  if ($cgi->{'parse'} && $recipe ne '_aggregate' && $recipe ne '_patchinfo') {
    local $Build::Rpm::includecallback = sub { getpackagesourceinfo_post_includecallback($files, @_) };
    sourceinfo_parse($r, $projid, $packid, $bconf, $recipe, $files->{$recipe}, $files->{'_service'});
  }
  return ($r, $BSXML::sourceinfo);
}

sub getpackagesourceinfo_post_cpio {
  my ($cgi, $projid, $packid, $cpiofn) = @_;
  local *F;
  open(F, '<', $cpiofn) || die("$cpiofn: $!\n");
  unlink($cpiofn);
  my $dir = "$uploaddir/$$.dir";
  mkdir_p($dir);
  my $uploaded = BSHTTP::cpio_receiver(BSHTTP::fd2req(\*F), {'directory' => $dir});
  close(F);
  my %files = map {$_->{'name'} => "$dir/$_->{'name'}"} @$uploaded;
  my @r = eval { getpackagesourceinfo_post_common($cgi, $projid, $packid, $cgi->{'filename'}, undef, \%files) };
  unlink("$dir/$_") for ls($dir);
  rmdir($dir) if -d $dir;
  die("$@\n") if $@;
  return @r;
}

sub getpackagesourceinfo_post {
  my ($cgi, $projid, $packid) = @_;
  mkdir_p($uploaddir);
  my $fn = "$uploaddir/$$";
  die("upload failed\n") unless BSServer::read_file($fn);
  # check if the upload is a cpio archive by looking at the first 6 bytes
  local *F;
  open(F, '<', $fn) || die("$fn: $!\n");
  my $magic;
  sysread(F, $magic, 6);
  close(F);
  return getpackagesourceinfo_post_cpio($cgi, $projid, $packid, $fn) if $magic eq '070701';
  if (!$cgi->{'filename'}) {
    unlink($fn);
    die("need a recipe filename\n");
  }
  my $files = { $cgi->{'filename'} => $fn };
  my @r = eval { getpackagesourceinfo_post_common($cgi, $projid, $packid, $cgi->{'filename'}, undef, $files) };
  unlink($fn);
  die("$@\n") if $@;
  return @r;
}

####################################################################

sub putconfiguration {
  my ($cgi) = @_;
  mkdir_p($uploaddir);
  my $uploaded = BSServer::read_file("$uploaddir/$$");
  die("upload failed\n") unless $uploaded;
  my $configuration = readxml("$uploaddir/$$", $BSXML::configuration);
  unlink("$uploaddir/$$");
  my $configurationxml = BSUtil::toxml($configuration, $BSXML::configuration);
  writestr("$BSConfig::bsdir/.configuration.xml", "$BSConfig::bsdir/configuration.xml", $configurationxml);

  # distribute to repo servers
  my @servers = BSSrcServer::Partition::allreposervers();
  push @servers, $BSConfig::serviceserver if $BSConfig::serviceserver;
  for my $server (@servers) {
    my $param = {
      'uri' => "$server/configuration",
      'request' => 'PUT',
      'data' => $configurationxml,
    };
    eval {
      BSRPC::rpc($param, undef);
    };
    warn($@) if $@;	# XXX: what now?
  }
  return $BSStdServer::return_ok;
}

sub getconfiguration {
  my $configuration = readxml("$BSConfig::bsdir/configuration.xml", $BSXML::configuration, 1) || {};
  return ($configuration, $BSXML::configuration);
}

####################################################################

sub putissuetrackers {
  my ($cgi) = @_;
  mkdir_p($uploaddir);
  my $uploaded = BSServer::read_file("$uploaddir/$$");
  die("upload failed\n") unless $uploaded;
  my $trackers = readxml("$uploaddir/$$", $BSXML::issue_trackers);
  unlink("$uploaddir/$$");
  writexml("$BSConfig::bsdir/.issuetrackers.xml", "$BSConfig::bsdir/issuetrackers.xml", $trackers, $BSXML::issue_trackers);
  return $BSStdServer::return_ok;
}

sub getissuetrackers {
  my $trackers = readxml("$BSConfig::bsdir/issuetrackers.xml", $BSXML::issue_trackers, 1) || {};
  return ($trackers, $BSXML::issue_trackers);
}

sub matchissues {
  my ($cgi) = @_;
  my $text = BSServer::read_data(100000000);
  my $trackers = readxml("$BSConfig::bsdir/issuetrackers.xml", $BSXML::issue_trackers, 1) || {};
  $trackers = BSSrcServer::Srcdiff::preparetrackers($trackers->{'issue-tracker'});
  my %issues;
  BSSrcServer::Srcdiff::issues($text, $trackers, \%issues);
  my @issues = map {$issues{$_}} sort keys %issues;
  BSSrcServer::Srcdiff::finalizeissues(@issues);
  return ({ 'issue' => \@issues },  $BSXML::issues);
}

####################################################################

sub orderkiwirepos_for_prio {
  my (@repos) = @_;
  my @prps;
  my %prps;
  for my $repo (@repos) {
    my $prp;
    my $url = ($repo->{'source'} || {})->{'path'};
    if ($url =~ /^obs:\/\/\/?([^\/]+)\/([^\/]+)\/?$/) {
      $prp = "$1/$2";
    } else {
      $prp = BSUrlmapper::urlmapper($url);
    }
    $prp ||= ':unknown/unknown';
    push @prps, $prp;
    push @{$prps{$prp}}, $repo;
  }
  @prps = BSUtil::unify(@prps);
  my %deps;
  for my $prp (@prps) {
    my ($projid, $repoid) = split('/', $prp, 2);
    my @path;
    eval {
      @path = expandsearchpath($projid, $repoid);
    };
    if ($@) {
      die($@) unless $@ =~ /^404/;
      warn($@);
    }
    my $oldrprp;
    for my $rprp (@path) {
      unshift @{$deps{$rprp}}, $oldrprp if $oldrprp && $rprp ne $oldrprp;
      $oldrprp = $rprp;
      if (!$prps{$rprp}) {
	$prps{$rprp} = [];
        push @prps, $rprp;
      }
    }
  }
  $deps{$_} = [ BSUtil::unify(@{$deps{$_}}) ]  for keys %deps;
  # hmm, depsort is not really stable...
  my @cycs;
  @prps = BSSolv::depsort(\%deps, undef, \@cycs, @prps);
  print "orderkiwirepo_for_prio cycle: ".join(' -> ', @$_)."\n" for @cycs;
  return map {@{$prps{$_}}} @prps;
}

sub orderkiwirepos {
  my ($cgi) = @_;
  mkdir_p($uploaddir);
  die("content read failed\n") unless BSServer::read_file("$uploaddir/$$");
  my $kiwi = readxml("$uploaddir/$$", $BSKiwiXML::kiwidesc);
  unlink("$uploaddir/$$");
  my %prios;
  my $pkgmanager;
  $pkgmanager = $kiwi->{'preferences'}->{'packagemanager'} if $kiwi->{'preferences'};
  $pkgmanager ||= 'zypper';
  for (@{$kiwi->{'repository'} || []}) {
    my $prio = $_->{'priority'};
    $prio = $pkgmanager eq 'smart' ? 0 : 99 unless defined $prio;
    $prio = 0 + $prio;
    $prio = -$prio if $pkgmanager eq 'smart';
    push @{$prios{$prio}}, $_;
  }
  if (%prios) {
    $kiwi->{'repository'} = [];
    for my $prio (sort {$a <=> $b} keys %prios) {
      push @{$kiwi->{'repository'}}, orderkiwirepos_for_prio(@{$prios{$prio}});
    }
  }
  return ($kiwi, $BSKiwiXML::kiwidesc);
}

####################################################################

sub add_redis_notifications {
  my ($payload) = @_;
  my $redisdir = "$eventdir/redis";
  mkdir_p($redisdir) unless -d $redisdir;
  my $fd;
  BSUtil::lockopen($fd, '>>', "$redisdir/queue");
  my $oldsize = -s $fd;
  for my $d (@$payload) {
    s/([\000-\037%|=\177-\237])/sprintf("%%%02X", ord($1))/ge for @$d;
    my $line = join('|', @$d)."\n";
    (syswrite($fd, $line) || 0) == length($line) || die("syswrite: $!\n");
  }
  close($fd);
  BSUtil::ping("$redisdir/.ping") unless $oldsize > 0;
}

sub external_notification_redis_dispatch {
  my ($conf, $req) = @_;
  die("redis keepalive process: must be POST:/notify/redis request\n") if "$req->{'action'}:$req->{'path'}" ne 'POST:/notify/redis';
  return external_notification_redis(BSDispatch::parse_cgi_singles($req));
}

sub start_redis_keepalive_process {
  my $req = $BSServer::request;
  # background us
  print "starting redis keepalive process\n";
  if (xfork()) {
    delete $req->{'__do_keepalive'};	# just finish normally
  } else {
    BSServer::serverstatus_close();	# no longer using the slot
    delete $req->{'starttime'};		# don't log twice
    $req->{'conf'}->{'dispatch'} = \&external_notification_redis_dispatch;	# only allow redist notifications
  }
}

sub external_notification_redis {
  my ($cgi) = @_;
  die("need payload for redis notification\n") unless BSServer::have_content();
  my $payload = BSUtil::fromstorable(BSServer::read_data());
  add_redis_notifications($payload);
  my $req = $BSServer::request;
  $req->{'allow_keepalive'} = 1 if $BSConfig::srcserver_notification_keepalive_maxcount && $cgi->{'keepalive'} && ($req->{'keepalive_count'} || 0) < $BSConfig::srcserver_notification_keepalive_maxcount && ($req->{'keepalive_starttime'} || 0) + 300 < time();
  $req->{'keepalive_maxidle'} = $req->{'keepalive_maxage'} = 300;
  BSStdServer::stdreply($BSStdServer::return_ok);
  start_redis_keepalive_process() if $req->{'__do_keepalive'} && !$req->{'keepalive_count'};
  return undef;
}

# FIXME improve error handling
sub verify_sourcepublish_event {
  my ($projid, $packid) = @_;
  BSVerify::verify_projid($projid);
  BSVerify::verify_packid($packid);
  BSSrcServer::Access::checksourceaccess($projid, $packid);
}

sub sourcepublish_isblocked {
  my ($projid, $packid, $projcache, $musthave) = @_;

  # check for exact match
  if (grep {$packid eq $_} @$BSConfig::sourcepublish_blocklist) {
    BSUtil::printlog("block list hit for $packid");
    return 1;
  }

  # extra check for maintenance projects
  my $proj = $projcache->{$projid};
  if (!$proj && !exists($projcache->{$projid})) {
    $proj = $projcache->{$projid} = readxml("$projectsdir/$projid.xml", $BSXML::proj, 1);
    if (!$proj) {
      die("400 Unable to parse $projid.xml\n") if $musthave;
      return undef;	# take it...
    }
  }
  return 0 unless ($proj->{'kind'} || '') eq 'maintenance_release';

  if (grep {$packid =~ /^\Q$_\E\./} @$BSConfig::sourcepublish_blocklist) {
    BSUtil::printlog("block list hit for $packid in maintenance release project $projid");
    return 1;
  }
  return 0;
}

sub external_notification_sourcepublish {
  my ($cgi) = @_;
  die("need payload for sourcepublish notification\n") unless BSServer::have_content();
  my $payload = BSUtil::fromstorable(BSServer::read_data());

  my $sourcepublishdir = "$eventdir/sourcepublish";
  mkdir_p($sourcepublishdir) unless -d $sourcepublishdir;

  my %projcache;
  my %blockedcache;
  my %verifycache;

  for (sort keys %$payload) {
    my ($projid, $packid, $md5) = split('/', $_);
    if (!exists($verifycache{"$projid/$packid"})) {
      verify_sourcepublish_event($projid, $packid, $md5);
      $verifycache{"$projid/$packid"} = 1;
    }
    BSVerify::verify_md5($md5);

    if ($BSConfig::sourcepublish_blocklist) {
      my $isblocked = $blockedcache{"$projid/$packid"};
      $isblocked = $blockedcache{"$projid/$packid"} = sourcepublish_isblocked($projid, $packid, \%projcache, 1) unless defined $isblocked;
      next if $isblocked;
    }

    my @included;
    for my $source (@{$payload->{$_} || []}) {
      my ($iprojid, $ipackid, $imd5) = split('/', $source);
      if (!exists($verifycache{"$iprojid/$ipackid"})) {
        verify_sourcepublish_event($iprojid, $ipackid);
        $verifycache{"$iprojid/$ipackid"} = 1;
      }
      BSVerify::verify_md5($imd5);
      if ($BSConfig::sourcepublish_blocklist) {
        my $isblocked = $blockedcache{"$projid/$packid"};
        $isblocked = $blockedcache{"$projid/$packid"} = sourcepublish_isblocked($projid, $packid, \%projcache) unless defined $isblocked;
        next if $isblocked;
      }
      push @included, { 'project' => $iprojid, 'package' => $ipackid, 'srcmd5' => $imd5 };
    }

    my $evname = "${projid}::${packid}::${md5}";
    my $ev = { 'type' => "sourcepublish", 'project' => $projid, 'package' => $packid, 'srcmd5' => $md5 };
    $ev->{'included'} = \@included if @included;
    writexml("$sourcepublishdir/.$evname$$", "$sourcepublishdir/$evname", $ev, $BSXML::event);
  }
  BSUtil::ping("$sourcepublishdir/.ping");
  return $BSStdServer::return_ok;
}

sub external_notification {
  my ($cgi, $type) = @_;
  return external_notification_redis($cgi) if $type eq 'redis';
  return external_notification_sourcepublish($cgi) if $type eq 'sourcepublish';
  my $param = {};
  for (keys %$cgi) {
    $param->{$_} = $cgi->{$_} unless $_ eq '_type' || /^\./;
  }
  my $payload;
  $payload = Storable::thaw(BSServer::read_data()) if $type eq 'PACKTRACK' && BSServer::have_content();
  notify($type, $param, $payload);
  return $BSStdServer::return_ok;
}

sub notify_plugins {
  my ($cgi, $type) = @_;
  my $param = {};
  if (BSServer::have_content()) {
    my $jsonbody = BSServer::read_data(10000000);
    $param = JSON::XS::decode_json($jsonbody);
  } else {
    for (keys %$cgi) {
      $param->{$_} = $cgi->{$_} unless $_ eq '_type' || /^\./;
    }
  }
  BSSrcServer::Notify::notify_plugins($type, $param);
  return $BSStdServer::return_ok;
}


####################################################################

sub listjobs {
  my ($cgi, $arch) = @_;
  die("not masterdispatching\n") unless $BSConfig::masterdispatcher;
  my $dir = BSRPC::rpc({
    'uri'     => "$BSConfig::masterdispatcher/jobs/$arch",
    'timeout' => 60,
  }, $BSXML::dir);
  my @jobs = sort(map {$_->{'name'}} @{$dir->{'entry'} || []});
  if ($cgi->{'partition'}) {
    my %good = map {$_ => 1} @{$cgi->{'partition'}};
    my %projid2partition;
    for my $job (splice @jobs) {
      my $jn = $job;
      $jn =~ s/-[0-9a-f]{32}$//s;
      my ($projid, $repoid, $packid) = split('::', $jn);
      next unless defined $repoid;
      my $part = $projid2partition{$projid};
      $part = $projid2partition{$projid} = BSSrcServer::Partition::projid2partition($projid) unless defined $part;
      push @jobs, $job if $good{$part};
    }
  }
  @jobs = map {{'name' => $_}} @jobs;
  return return ({'entry' => \@jobs}, $BSXML::dir);
}

####################################################################

sub hello {
  my ($cgi) = @_;
  return "<hello name=\"Source Repository Ajax Server\" repoid=\"$datarepoid\" />\n" if $BSStdServer::isajax;
  return "<hello name=\"Source Repository Server\" repoid=\"$datarepoid\" />\n";
}

####################################################################

sub getworkercap {
  my ($cgi, $workerid) = @_;
  my $reposerver = $BSConfig::masterdispatcher || $BSConfig::reposerver; # no need to look for partitioning here
  my $param = {
    'uri' => "$reposerver/worker/$workerid",
    'timeout' => 60,
  };

  my $worker_cap = BSWatcher::rpc($param, $BSXML::worker);

  return($worker_cap, $BSXML::worker);
}

sub checkconstraints {
  my ($cgi) = @_;
  checkprojrepoarch($cgi->{'project'}, $cgi->{'repository'}, $cgi->{'arch'});

  my $reposerver = $BSConfig::masterdispatcher;
  $reposerver ||= $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($cgi->{'project'}) : $BSConfig::reposerver;

  my @args = BSRPC::args($cgi, 'project', 'repository', 'arch', 'package');

  my $param = {
    'uri' => "$reposerver/worker",
    'request' => 'POST',
  };
  if (BSServer::have_content()) {
    $param->{'data'} = \&BSServer::forward_sender;
    $param->{'chunked'} = 1;
  } else {
      my $rev = getrev($cgi->{'project'}, $cgi->{'package'}, $cgi->{'rev'});
      my $files = lsrev_expanded($rev);
      if ($files->{'_constraints'}) {
        $param->{'data'} = BSRevision::revreadstr($rev, '_constraints', $files->{'_constraints'});
      }
  }

  my $ret = BSWatcher::rpc($param, $BSXML::dir, "cmd=checkconstraints", @args);
  return ($ret, $BSXML::dir);
}

sub getavailable {
  my ($in, $available) = @_;
  for my $i (@{$in || []}) {
    for my $arch (@{$i->{'arch'} || []}) {
      $available->{$_}->{$arch} = 1 for @{$i->{'name'} || []};
    }
  }
}

sub processavailable {
  my ($available) = @_;
  my %archlist;
  my @res;
  for my $bin (sort keys %$available) {
    my $archlist = join(',', sort keys %{$available->{$bin}});
    $archlist{$archlist}->{$bin} = 1;
  }
  for my $archlist (sort keys %archlist) {
    my @archs = split(',', $archlist);
    push @res, {'arch' => \@archs, 'name' => [ sort keys %{$archlist{$archlist}} ]};
  }
  return \@res;
}

sub getavailablebinaries {
  my ($cgi, $projid) = @_;
  my @path = @{$cgi->{'path'} || []};
  for my $url (@{$cgi->{'url'} || []}) {
    if ($url =~ /^obs:\/\/\/?([^\/]+)\/([^\/]+)\/?$/) {
      push @path, "$1/$2";
    } else {
      my $prp = BSUrlmapper::urlmapper($url);
      push @path, $prp if $prp;
    }
  }
  my @prpa;
  my @arch = @{$cgi->{'arch'} || []};
  my %archfilter = map {$_ => 1} @arch;
  # hack ahead
  my $proj = BSRevision::readproj_local($projid, 1) || {};
  for my $repo (@{$proj->{'repository'} || []}) {
    my @repoarch = @{$repo->{'arch'} || []};
    next unless @repoarch;
    if (@{$proj->{'repository'} || []} > 1) {
      next unless $repo->{'name'} && $repo->{'name'} =~ /^images/;
    }
    push @arch, @repoarch unless %archfilter;
    if (!@path) {
      for my $prp (expandsearchpath($projid, $repo->{'name'})) {
        push @prpa, map {"$prp/$_"} @repoarch;
      }
    }
  }
  if (@path) {
    @arch = BSUtil::unify(@arch);
    for my $prp (@path) {
      push @prpa, map {"$prp/$_"} @arch;
    }
  }
  @prpa = BSUtil::unify(@prpa);

  # now partition the prpas
  my %projid2reposerver;
  my %prpas_by_reposerver;
  my %remoteprojects;
  for my $prpa (@prpa) {
    my ($aprojid, $arepoid, $aarch) = split('/', $prpa, 3);
    my $reposerver = $projid2reposerver{$aprojid};
    if (!$reposerver) {
      # check if it's a remote repo
      my $proj = BSRevision::readproj_local($aprojid, 1);
      $proj = BSSrcServer::Remote::remoteprojid($aprojid) if !$proj || $proj->{'remoteurl'};
      if ($proj->{'remoteurl'}) {
	$reposerver = "remote://$proj->{'remoteurl'}";
	$remoteprojects{$aprojid} = $proj;
      } else {
        $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($aprojid) : $BSConfig::reposerver;
      }
      $projid2reposerver{$aprojid} = $reposerver;
    }
    push @{$prpas_by_reposerver{$reposerver}}, $prpa;
  }

  # go get em
  my (%available, %available_pattern, %available_product);
  for my $reposerver (sort keys %prpas_by_reposerver) {
    my @args;
    push @args, map {"prpa=$_"} @{$prpas_by_reposerver{$reposerver}};
    push @args, "cmd=availablebinaries";
    my $param = {
      'uri' => "$reposerver/_command",
      'timeout' => 30,
      'request' => 'POST',
    };
    if ($reposerver =~ /^remote:\/\//) {
      my $firstproj;
      my @remotepath;
      my @remotearch;
      for my $prpa (@{$prpas_by_reposerver{$reposerver}}) {
	my ($aprojid, $arepoid, $aarch) = split('/', $prpa, 3);
	my $proj = $remoteprojects{$aprojid};
	next unless $proj;
	$firstproj ||= $proj;
	push @remotepath, "$proj->{'remoteproject'}/$arepoid";
	push @remotearch, $aarch;
      }
      next unless $firstproj && @remotepath && @remotearch;
      $param = {
	'uri' => "$firstproj->{'remoteurl'}/build/$firstproj->{'remoteproject'}/_availablebinaries",
	'timeout' => 30,
	'request' => 'GET',
        'proxy' => $firstproj->{'remoteproxy'},
      };
      @args = ();
      push @args, map {"path=$_"} BSUtil::unify(@remotepath);
      push @args, map {"arch=$_"} BSUtil::unify(@remotearch);
    }
    my $res;
    eval {
      $res = BSWatcher::rpc($param, $BSXML::availablebinaries, @args);
    };
    warn($@) if $@;
    next unless $res;
    #return ($res, $BSXML::availablebinaries) if keys(%prpas_by_reposerver) == 1;
    getavailable($res->{'packages'}, \%available) if $res->{'packages'};
    getavailable($res->{'patterns'}, \%available_pattern) if $res->{'patterns'};
    getavailable($res->{'products'}, \%available_product) if $res->{'products'};
  }
  my %res;
  $res{'packages'} = processavailable(\%available) if %available;
  $res{'patterns'} = processavailable(\%available_pattern) if %available_pattern;
  $res{'products'} = processavailable(\%available_product) if %available_product;
  return (\%res, $BSXML::availablebinaries);
}

####################################################################

sub registry_ownrepo {
  my ($cgi, $projid, $repoid, $regrepo) = @_;
  my $prp = "$projid/$repoid";
  my $oprp = BSSrcServer::Registry::ownrepo($prp, $regrepo);
  my ($oprojid, $orepoid) = split('/', $oprp, 2);
  my $owner = { 'regrepo' => $regrepo, 'project' => $oprojid, 'repository' => $orepoid };
  return ($owner, $BSXML::regrepoowner);
}

sub registry_disownrepo {
  my ($cgi, $projid, $repoid, $regrepo) = @_;
  my $prp = "$projid/$repoid";
  BSSrcServer::Registry::disownrepo($prp, $regrepo);
  return $BSStdServer::return_ok;
}

sub registry_forward {
  my ($cgi, $regrepo) = @_;
  my $repserver = BSSrcServer::Registry::regrepo2reposerver($regrepo);
  die("404 NAME_UNKNOWN\n") unless $repserver;
  my $req = $BSServer::request;
  my $path = $req->{'path'};
  die("400 NAME_INVALID") unless $path =~ s/^\/[^\/]+\///;	# strip 'v2' or 'registry'
  my @hdrs;
  my $range = BSServer::header('Range');
  push @hdrs, "Range: $range" if $range;
  my $accept = BSServer::header('Accept');
  push @hdrs, "Accept: $accept" if $accept;
  my $param = {
    'uri' => "$repserver/registry/$path",
    'request' => $req->{'action'},
    'headers' => \@hdrs,
    'ignorestatus' => 1,
    'receiver' => \&BSServer::reply_receiver,
    'reply_receiver_forward_hdrs' => 1,
  };
  my @args = BSRPC::args($cgi, 'n', 'last', 'artifactType');
  BSWatcher::rpc($param, undef, @args);
  return undef;
}

sub registry_manifestinfo {
  my ($cgi, $regrepo, $manifest) = @_;
  
  my $foundone;
  if ($BSConfig::publish_containers && $BSConfig::container_registries && $regrepo =~ /^([^\/]+\.[^\/]+)\/(.*)$/) {
    my $gun = $1;
    my %regs;
    my @pc = @$BSConfig::publish_containers;
    while (@pc) {
      my (undef, $v) = splice(@pc, 0, 2);
      $regs{$_} = 1 for @{ref($v) ? $v : [ $v ]};
    }
    delete $regs{'local'};
    for my $regname (sort keys %regs) {
      my $registry = $BSConfig::container_registries->{$regname};
      next unless $registry && $registry->{'manifestinfos'};
      my $reggun = $registry->{'notary_gunprefix'} || $registry->{'server'};
      $reggun =~ s/^https?:\/\///;
      next unless $gun eq $reggun;
      $foundone = 1;
      last;
    }
  }
  return registry_forward($cgi, $regrepo) if !$foundone || BSSrcServer::Registry::regrepo2reposerver($regrepo);

  my %reposervers = map {$_ => 1} values(%$BSConfig::partitionservers);
  for my $reposerver (sort keys %reposervers) {
    my $param = {
      'uri' => "$reposerver/registry/$regrepo/manifestinfos/$manifest",
      'timeout' => 60,
    };
    my $info = eval { BSWatcher::rpc($param, undef) };
    next unless $info;
    BSRegistryServer::reply($info, 'Content-Type: application/json');
    return undef;
  }
  die("404 MANIFESTINFO_UNKNOWN\n");
}

sub registry_catalog {
  my ($cgi) = @_;
  my @repos = BSSrcServer::Registry::catalog();
  my @hdrs;
  BSRegistryServer::paginate($cgi, \@repos, "/v2/_catalog", \@hdrs);
  my $ret = {
    'repositories' => \@repos,
  };
  BSRegistryServer::reply($ret, @hdrs);
  return undef;
}

sub registry_rootinfo {
  my ($cgi) = @_;
  my $rootinfo = BSSrcServer::Registry::rootinfo();
  BSRegistryServer::reply($rootinfo);
  return undef;
}

sub registry_version {
  my ($cgi) = @_;
  my @hdrs;
  push @hdrs, 'X-Registry-Supports-Signatures: 1' if $BSConfig::local_registry_signatures_extension;
  BSRegistryServer::reply({}, @hdrs);
  return undef;
}

sub notary_forward {
  my ($cgi, $gunprefix, $regrepo, $filename) = @_;
  registry_forward($cgi, $regrepo);
  return undef;
}

sub sigstore_forward {
  my ($cgi, $regrepo, $sig) = @_;
  my $req = $BSServer::request;
  die("404 not found\n") unless $req->{'path'} =~ s/\@([^\@\/]+)\/([^\@\/]+)$/\/signatures\/$2\@$1/;
  die("404 not found\n") unless $regrepo =~ s/\@[^\@\/]+$//;
  registry_forward($cgi, $regrepo);
  return undef;
}

####################################################################

sub slsa_addrefs {
  my ($cgi, $refprpa) = @_;
  my $refs = BSUtil::fromstorable(BSServer::read_data(1000000000));
  my %servers;
  for my $prpa (sort keys %$refs) {
    my ($projid) = split('/', $prpa);
    $projid = (split('/', $refprpa))[0] if $prpa eq '_config';	# keep extra config data
    my $proj = BSRevision::readproj_local($projid, 1);
    $proj = BSSrcServer::Remote::remoteprojid($projid) if !$proj || $proj->{'remoteurl'};
    if ($proj->{'remoteurl'}) {
      # skip interconnect references for now
      next;
    }
    my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
    $servers{$reposerver}->{$prpa} = $refs->{$prpa};
  }
  for my $server (sort keys %servers) {
    my $param = {
      'uri' => "$server/slsa",
      'request' => 'POST',
      'data' => BSUtil::tostorable($servers{$server}),
    };
    BSRPC::rpc($param, undef, "cmd=addrefs", "prpa=$refprpa");
  }
  return $BSStdServer::return_ok;
}

sub slsa_getfile {
  my ($cgi, $projid, $repoid, $arch, $filename, $digest) = @_;
  my $proj = BSRevision::readproj_local($projid, 1);
  $proj = BSSrcServer::Remote::remoteprojid($projid) if !$proj || $proj->{'remoteurl'};
  if ($proj->{'remoteurl'}) {
    die("404 no remote slsa references\n");
  }
  my $reposerver = $BSConfig::partitioning ? BSSrcServer::Partition::projid2reposerver($projid) : $BSConfig::reposerver;
  my @args = BSRPC::args($cgi, 'view');
  my $param = {
    'uri' => "$reposerver/slsa/$projid/$repoid/$arch/$filename/$digest",
    'ignorestatus' => 1,
    'receiver' => \&BSServer::reply_receiver,
  };
  BSWatcher::rpc($param, undef, @args);
  return undef;
}

####################################################################

sub run {
  my ($conf) = @_;
  BSSrcServer::SQLite::init_publisheddb($extrepodb) if $BSConfig::published_db_sqlite;
  BSSrcServer::SQLite::init_sourcedb($sourcedb) if $BSConfig::source_db_sqlite;
  BSServer::server($conf);
}


sub periodic {
  my ($conf) = @_;
  BSStdServer::periodic($conf);
  my $evdir = "$eventdir/undelivered";
  if (-d $evdir && BSUtil::lockcheck('>>', "$evdir/.lock")) {
    print "delivering queued events...\n";
    if (xfork() == 0) {
      my $lfd;
      BSUtil::lockopen($lfd, '>>', "$evdir/.lock");
      notify_repservers_send_queued_events($evdir);
      sleep(10);
      unlink("$evdir/.lock");
      close $lfd;
      rmdir($evdir);
      exit(0);
    }
  }
}

my $dispatches = [
  '/' => \&hello,

  '!rw :' => undef,
  '!- GET:' => undef,
  '!- HEAD:' => undef,

  'POST:/source cmd=orderkiwirepos' => \&orderkiwirepos,
  'POST:/source cmd=triggerscmsync scmrepository:? scmbranch:? isdefaultbranch:?' => \&triggerscmsync,
  'POST:/source cmd: *:*' => \&unknowncmd,

  # /source name space: manage project and package data
  '/source deleted:bool? project*' => \&getprojectlist,

  'POST:/source/$project cmd=createkey user:? comment:?' => \&createkey,
  'POST:/source/$project cmd=extendkey user:? comment:? days:num?' => \&extendkey,
  'POST:/source/$project cmd=recreatesslcert user:? comment:?' => \&recreatesslcert,
  'POST:/source/$project cmd=undelete user:? comment:?' => \&undeleteproject,
  'POST:/source/$project cmd=copy user:? comment:? oproject:project withbinaries:bool? withhistory:bool? makeolder:bool? makeoriginolder:bool? resign:bool? noservice:bool?' => \&copyproject,
  'POST:/source/$project cmd=move oproject:project' => \&moveproject,
  'POST:/source/$project cmd=freezelink user:? comment:? requestid:num?' => \&freezeprojectlink,
  'POST:/source/$project cmd=notifypackagechange' => \&notifypackagechange,
  'POST:/source/$project cmd: *:*' => \&unknowncmd,
  '/source/$project view=info parse:bool? nofilename:bool? repository? arch? package* withchangesmd5:bool? noexpand:bool? withmetamd5:bool?' => \&getprojectsourceinfo,
  '/source/$project deleted:bool? expand:bool? noorigins:bool?' => \&getpackagelist,

  'DELETE:/source/$project user:? comment:? requestid:num?' => \&delproject,
  '/source/$project/_meta rev? withconfig:bool? interconnect:?' => \&getproject,
  'PUT:/source/$project/_meta user:? comment:? requestid:num? lowprio:bool?' => \&putproject,

  '/source/$project/_pubkey rev?' => \&getpubkey,
  'DELETE:/source/$project/_pubkey user:? comment:?' => \&deletekey,

  '/source/$project/_config rev?' => \&getprojectconfig,
  'PUT:/source/$project/_config user:? comment:?' => \&putprojectconfig,
  'DELETE:/source/$project/_config user:? comment:?' => \&delprojectconfig,

  '/source/$project/_history rev? meta:bool? deleted:bool? limit:num? startbefore:num?' => \&getpackagehistory,
  '/source/$project/_keyinfo withsslcert:bool? autoextend:bool? donotcreatecert:bool? signtype:? signflavor:?' => \&getkeyinfo,

  'POST:/source/$project/$package view=info parse:bool? repository? arch? filename?' => \&getpackagesourceinfo_post,

  'POST:/source/$project/$package cmd=diff rev? orev:rev? oproject:project? opackage:package? expand:bool? linkrev? olinkrev:? unified:bool? missingok:bool? meta:bool? file:* filelimit:num? tarlimit:num? view:? withissues:bool? onlyissues:bool? nocache:bool? cacheonly:bool? nodiff:bool?' => \&sourcediff,
  'POST:/source/$project/$package cmd=linkdiff rev? linkrev? unified:bool? file:* filelimit:num? tarlimit:num? view:? withissues:bool? onlyissues:bool? nocache:bool? cacheonly:bool? nodiff:bool?' => \&linkdiff,
  'POST:/source/$project/$package cmd=servicediff rev? unified:bool? file:* filelimit:num? tarlimit:num? view:? withissues:bool? onlyissues:bool? nocache:bool? cacheonly:bool? nodiff:bool?' => \&servicediff,
  'POST:/source/$project/$package cmd=commit rev? user:? comment:? keeplink:bool? repairlink:bool? linkrev? setrev:bool? requestid:num? noservice:bool?' => \&sourcecommit,
  'POST:/source/$project/$package cmd=commitfilelist rev? user:? comment:? keeplink:bool? repairlink:bool? linkrev? setrev:bool? requestid:num? time:num? version:? vrev:? noservice:bool? servicemark:? withvalidate:?' => \&sourcecommitfilelist,
  'POST:/source/$project/$package cmd=commitobsscm srcmd5? servicemark: run:md5 user:? comment:? info:?' => \&sourcecommitobsscm,
  'POST:/source/$project/$package cmd=copy rev? user:? comment:? orev:rev? oproject:project? opackage:package? expand:bool? keeplink:bool? repairlink:bool? linkrev? setrev:linkrev? olinkrev:linkrev? requestid:num? dontupdatesource:bool? noservice:bool? withvrev:bool? withacceptinfo:bool? makeoriginolder:bool? freezelink:bool? vrevbump:num? instantiate:bool?' => \&sourcecopy,
  'POST:/source/$project/$package cmd=collectbuildenv user:? comment:? orev:rev? oproject:project? opackage:package?' => \&sourcecollectbuildenv,
  'POST:/source/$project/$package cmd=branch rev? user:? comment:? orev:rev? oproject:project? opackage:package? olinkrev:linkrev? requestid:num? force:bool? keepcontent:bool? missingok:bool? noservice:bool? withacceptinfo:bool? time:num? extendvrev:bool? simplelink:bool?' => \&sourcebranch,
  'POST:/source/$project/$package cmd=linktobranch rev? user:? comment:? linkrev? noservice:bool?' => \&linktobranch,
  'POST:/source/$project/$package cmd=deleteuploadrev' => \&deleteuploadrev,
  'POST:/source/$project/$package cmd=undelete user:? comment:? time:num?' => \&undeletepackage,
  'POST:/source/$project/$package cmd=runservice user:? comment:?' => \&triggerservicerun,
  'POST:/source/$project/$package cmd=waitservice' => \&waitservicerun,
  'POST:/source/$project/$package cmd=mergeservice user:? comment:?' => \&mergeservicerun,
  'POST:/source/$project/$package cmd=getprojectservices' => \&getprojectservices,
  'POST:/source/$project/$package cmd=notifypackagechange' => \&notifypackagechange,
  'POST:/source/$project/$package cmd=lock user:? comment:?' => \&lockpackage,
  'POST:/source/$project/$package cmd=unlock user:? comment:?' => \&unlockpackage,
  'POST:/source/$project/$package cmd: *:*' => \&unknowncmd,

  'PUT:/source/$project/$package cmd: rev? user:? comment:?' => \&sourcecommitfilelist,	# obsolete

  '/source/$project/$package view=getmultibuild' => \&getmultibuildpackages,
  '/source/$project/$package view=info rev? linkrev? parse:bool? nofilename:bool? repository? arch? withchangesmd5:bool? noexpand:bool? withmetamd5:bool?' => \&getpackagesourceinfo,
  '/source/$project/$package rev? linkrev? emptylink:bool? deleted:bool? expand:bool? view:? extension:? lastworking:bool? withlinked:bool? meta:bool? product:?' => \&getfilelist,
  '/source/$project/$package/_history rev? meta:bool? deleted:bool? limit:num? startbefore:num?' => \&getpackagehistory,
  '/source/$project/$package/_meta rev? expand:bool? meta:bool? deleted:bool? view:? interconnect:?' => \&getpackage,
  'PUT:/source/$project/$package/_meta user:? comment:? requestid:num?' => \&putpackage,
  'DELETE:/source/$project/$package user:? comment:? requestid:num?' => \&delpackage,
  '/source/$project/$package/$filename rev? expand:bool? meta:bool? deleted:bool? view:? noajax:bool?' => \&getfile,
  'PUT:/source/$project/$package/$filename rev? user:? comment:? keeplink:bool? force:bool? meta:bool?' => \&putfile,
  'DELETE:/source/$project/$package/$filename rev? user:? comment:? keeplink:bool? force:bool? meta:bool?' => \&delfile,

  # /published name spec: access published binaries
  '/published view=collection name:* project*' => \&published_collection,
  '/published' => \&published,
  '/published/$project' => \&published,
  '/published/$project/$repository view=publishedpath medium:?' => \&published_path,
  '/published/$project/$repository view=status' => \&published_status,
  '/published/$project/$repository' => \&published,
  '/published/$project/$repository/$arch:filename view:?' => \&published,
  '/published/$project/$repository/$arch:filename/$filename view:?' => \&published,
  '/published/$project/$repository/$arch:filename/$filename/$subfilename:filename view:?' => \&published,

  # scheduler calls
  '/getprojpack $project* $repository* $package* $arch? withrepos:bool? withsrcmd5:bool? withdeps:bool? withconfig:bool? expandedrepos:bool? ignoredisable:bool? nopackages:bool? withremotemap:bool? remotemaponly:bool? noremote:bool? parseremote:bool? buildinfo:bool? partition:? view:?' => \&getprojpack,
  'POST:/relsync $project $repository $arch' => \&postrelsync,
  '/relsync $project $repository $arch' => \&getrelsync,

  # worker capabilities
  '/worker/$workerid' => \&getworkercap,
  'POST:/worker cmd=checkconstraints $project $repository $arch $package rev?' => \&checkconstraints,

  # worker calls
  '/getsources $project $package $srcmd5:md5 workerid?' => \&getsources,
  '/getconfig $project $repository path:prp* workerid?' => \&worker_getbuildconfig,
  '/getsslcert $project autoextend:bool? workerid? signtype:? signflavor:?' => \&getsslcert,
  '/getbinaries $project $repository $arch binaries: nometa:bool? workerid? now:num? module*' => \&worker_getbinaries,
  '/getbinaryversions $project $repository $arch binaries: nometa:bool? workerid? now:num? module* withevr:bool?' => \&worker_getbinaryversions,
  '/getobsgendiffdata $project $repository $arch jobid? workerid?' => \&getobsgendiffdata,
  '/getmodulemd $project $package $srcmd5:md5 module* modularityplatform: modularitylabel: arch? view:?' => \&getmodulemd,

  # worker/publisher/signer calls
  '/getsignkey $project withpubkey:bool? autoextend:bool? withalgo:bool? signtype:? signflavor:?' => \&getsignkey,

  # event/notification handling
  '!- /lastevents $filter:* start:num? obsname:? client:? block:bool?' => \&lastevents,
  '!- /lasteventsproxy $filter:* start:num? remoteurl: client:?' => \&lasteventsproxy,
  '/lastnotifications start:num? view:? block:bool? noprune:bool?' => \&lastnotifications,
  '/notificationpayload/$payloadkey:filename' => \&getnotificationpayload,
  'DELETE:/notificationpayload/$payloadkey:filename' => \&deletenotificationpayload,
  'POST:/event type: project: package:? repository:? arch:? job:?' => \&newevent,
  # tmp until lightty gets fixed
  '/public/lastevents $filter:* start:num? obsname:? block:bool? client:?' => \&lastevents,

  # search interface
  '/search $in: $match: return:? values:bool?' => \&search,
  '/search/project $match:' => \&search_proj,
  '/search/project/id $match:' => \&search_proj_id,
  '/search/package $match:' => \&search_pack,
  '/search/package/id $match:' => \&search_pack_id,

  'POST:/search/published cmd:' => \&search_published_updatedb,
  '/search/published/binary/id $match: limit:num? withdownloadurl:bool?' => \&search_published_binary_id,
  '/search/published/binary/name $match: limit:num?' => \&search_published_binary_name,
  '/search/published/pattern/id $match: limit:num? withdownloadurl:bool?' => \&search_published_pattern_id,
  '/search/published/repoinfo/id $match: limit:num? withdownloadurl:bool?' => \&search_published_repoinfo_id,

  # service interface, just for listing for now
  '/service' => \&listservices,
#  '/service/$service' => \&service,

  # configuration
  'PUT:/configuration' => \&putconfiguration,
  '/configuration' => \&getconfiguration,

  # issue trackers
  'PUT:/issue_trackers' => \&putissuetrackers,
  'POST:/issue_trackers cmd=issues' => \&matchissues,
  '/issue_trackers' => \&getissuetrackers,

  # build calls for binary files
  '/build' => \&getprojectlist,
  '/build/_workerstatus scheduleronly:bool? daemonsonly:bool? arch*' => \&getworkerstatus,
  'PUT:/build/_dispatchprios' => \&putdispatchprios,
  '/build/_dispatchprios' => \&getdispatchprios,
  '/build/_result scmrepository:? scmbranch:? code:? lastbuild:bool? multibuild:bool? locallink:bool?' => \&getscmresult,
  '/build/$project/_availablebinaries url:* path:prp* arch*' => \&getavailablebinaries,
  'POST:/build/$project cmd: repository* arch* package* code:* lastbuild:bool? wipe:* comment:? sysrq:?' => \&docommand,
  '/build/$project' => \&getrepositorylist,
  '/build/$project/_result oldstate:md5? view:resultview* lastbuild:bool? repository* arch* package* code:* multibuild:bool? locallink:bool?' => \&getresult,
  '/build/$project/_jobhistory package* code:* limit:num? endtime_start:num? endtime_end:num?' => \&getjobhistory_project,
  '/build/$project/$repository' => \&getarchlist,
  '/build/$project/$repository/_buildconfig path:prp*' => \&getbuildconfig,
  '/build/$project/$repository/$arch package* view:?' => \&getpackagelist_build,
  '!- /build/$project/$repository/$arch/_builddepinfo package* view:?' => \&getbuilddepinfo,
  '/build/$project/$repository/$arch/_jobhistory package* code:* limit:num? endtime_start:num? endtime_end:num?' => \&getjobhistory,
  'POST:/build/$project/$repository/$arch/_repository cmd=missingdodresources resource:* partition:?' => \&missingdodresources,
  'POST:/build/$project/$repository/$arch/_repository match:' =>  \&postrepo,
  'POST:/build/$project/$repository/$arch/$package cmd=copy oproject:project? opackage:package? orepository:repository? setupdateinfoid:? resign:bool? setrelease:? multibuild:bool?' => \&copybuild,
  'POST:/build/$project/$repository/$arch/$package' => \&uploadbuild,
  '/build/$project/$repository/$arch/$package_repository view:? binary:filename* nometa:bool? nosource:bool? withmd5:bool? module* withccache:bool? withevr:bool?' => \&getbinarylist,
  'POST:/build/$project/$repository/$arch/$package_repositorybuild/_buildinfo add:* debug:bool? striphdrmd5:bool?' => \&getbuildinfo_post,
  '/build/$project/$repository/$arch/$package/_buildinfo add:* internal:bool? debug:bool? striphdrmd5:bool?' => \&getbuildinfo,
  '/build/$project/$repository/$arch/$package/_jobstatus' => \&getjobstatus,
  '/build/$project/$repository/$arch/$package/_log nostream:bool? last:bool? lastsucceeded:bool? start:intnum? end:num? view:?' => \&getlogfile,
  '/build/$project/$repository/$arch/$package/_reason' => \&getbuildreason,
  '/build/$project/$repository/$arch/$package/_status' => \&getbuildstatus,
  '/build/$project/$repository/$arch/$package/_history limit:num?' => \&getbuildhistory,
  '/build/$project/$repository/$arch/$package/_buildstats limit:num?' => \&getbuildstats,
  '/build/$project/$repository/$arch/$package_repository/$filename view:? withfilelist:bool? module*' => \&getbinary,
  'PUT:/build/$project/$repository/$arch/_repository/$filename ignoreolder:bool? wipe:bool?' => \&putbinary,
  'DELETE:/build/$project/$repository/$arch/_repository/$filename' => \&delbinary,

  '/request/_lastid' => \&getlastidrequest,	# just required for migration into api
  '/request/$id:num' => \&getrequest, 		# just required for migration into api

  # for masterdispatcher syncing
  '/jobs/$arch partition:*' => \&listjobs,

  # notifications from publisher/repserver - CGI
  'POST:/notify/$_type: *:?' => \&external_notification,
  # called from the API to notify hermes/rabbitmq
  'POST:/notify_plugins/$_type:' => \&notify_plugins,

  # publisher registry own/disown
  'POST:/ownregistryrepo $project $repository $regrepo' => \&registry_ownrepo,
  'POST:/disownregistryrepo $project $repository $regrepo' => \&registry_disownrepo,

  # registry
  'GET|HEAD:/registry/$gunprefix:/$...:regrepo/_trust/tuf/$filename' => \&notary_forward,
  'GET|HEAD:/registry/$...:regrepo/manifests/$manifest: ns:?' => \&registry_forward,
  'GET|HEAD:/registry/$...:regrepo/manifestinfos/$manifest: ns:?' => \&registry_manifestinfo,
  'GET|HEAD:/registry/$...:regrepo/blobs/$blob: ns:?' => \&registry_forward,
  'GET|HEAD:/registry/$...:regrepo/signatures/$sig: ns:?' => \&registry_forward,
  'GET|HEAD:/registry/$...:regrepo/referrers/$subject: ns:? artifactType:?' => \&registry_forward,
  'GET|HEAD:/registry/$...:regrepo/tags/list n:intnum? last:? ns:?' => \&registry_forward,
  'GET|HEAD:/registry/$...:regrepo/info.json ns:?' => \&registry_forward,
  'GET|HEAD:/registry/_catalog n:intnum? last:? ns:?' => \&registry_catalog,
  'GET|HEAD:/registry/info.json ns:?' => \&registry_rootinfo,
  'GET|HEAD:/registry ns:?' => \&registry_version,
  'GET|HEAD:/sigstore/$...:regrepo/$sig:' => \&sigstore_forward,

  # slsa
  'POST:/slsa cmd=addrefs $prpa' => \&slsa_addrefs,
  '/slsa/$project/$repository/$arch/$filename/$digest: view:?' => \&slsa_getfile,

  '/ajaxstatus aidx=all' => \&getajaxstatuslist,
  '/ajaxstatus aidx:num?' => \&getajaxstatus,
  '/serverstatus' => \&BSStdServer::serverstatus,
];

####################################################################

my $dispatches_ajax = [
  '/' => \&hello,
  '/ajaxstatus' => \&getajaxstatus,
  '/build/$project/_result oldstate:md5? view:resultview* repository* arch* package* code:*' => \&getresult,
  '/build/$project/$repository/$arch package* view:?' => \&getpackagelist_build,
  '/build/$project/$repository/$arch/$package/_log nostream:bool? last:bool? lastsucceeded:bool? start:intnum? end:num?' => \&getlogfile,
  '/build/$project/$repository/$arch/$package_repository view:? binary:filename* nometa:bool? nosource:bool? withmd5:bool? module* withccache:bool? withevr:bool?' => \&getbinarylist,
  '/getbinaries $project $repository $arch binaries: nometa:bool? raw:bool? now:num? module*' => \&worker_getbinaries,
  '/getbinaryversions $project $repository $arch binaries: nometa:bool? now:num? module* withevr:bool?' => \&worker_getbinaryversions,
  '/getconfig $project $repository path:prp* workerid?' => \&getbuildconfig_ajax,
  '/lastevents $filter:* start:num? obsname:? client:?' => \&lastevents,
  '/lasteventsproxy $filter:* start:num? remoteurl: client:?' => \&lasteventsproxy,
  '/lastnotifications start:num? view:? block:bool?' => \&lastnotifications,
  '/source/$project/$package cmd=waitservice servicemark:' => \&waitservicerun,
  '/source/$project/$package rev view:' => \&getfilelist_ajax,
  '/source/$project/$package:package/$filename rev?' => \&getfile,
  '/request/$id:num withkey:bool? oldkey:md5?' => \&getrequest,
  '/sourcediffcache/$cacheid:md5 view:? file:*' => \&getsourcediffcache,
];

####################################################################

my $conf = {
  'runname' => 'bs_srcserver',
  'port' => $port,
  'proto' => $proto,
  'dispatches' => $dispatches,
  'maxchild' => 20,
  'maxchild2' => 20,
  'slowrequestthr' => 10,
  'errorreply' => \&BSRegistryServer::errreply,
  'run' => \&run,
  'periodic' => \&periodic,
  'client_time_header' => 'X-Frontend-Start',
};

my $aconf = {
  'socketpath' => $ajaxsocket,
  'dispatches' => $dispatches_ajax,
  'slowrequestthr' => 300,
};

$conf->{'maxchild'} = $BSConfig::srcserver_maxchild if $BSConfig::srcserver_maxchild;
$aconf->{'partitions'} = $BSConfig::srcserver_ajaxpartitions if $BSConfig::srcserver_ajaxpartitions;

if ($BSConfig::workersrcserver) {
  my $wport = $port;
  $wport = $1 if $BSConfig::workersrcserver =~ /:(\d+)$/;
  $conf->{'port2'} = $wport if $wport != $port;
  my $wproto = 'http';
  $wproto = $1 if $BSConfig::workersrcserver =~ /^(https):/;
  die("worker port on $wproto but normal port on $proto: both need to be the same\n") unless $wproto eq $proto;
}
%$conf = (%$conf, %{$BSConfig::srcserver_extraconf}) if $BSConfig::srcserver_extraconf;

# create bsdir before root privileges are dropped
BSUtil::mkdir_p_chown($BSConfig::bsdir, $BSConfig::bsuser, $BSConfig::bsgroup);

# set a repoid for identification of this data repository
if (! -e "$projectsdir/_repoid") {
  BSUtil::mkdir_p_chown($projectsdir, $BSConfig::bsuser, $BSConfig::bsgroup);
  $datarepoid = sprintf("%09d", int(rand(1000000000)));
  writestr("$projectsdir/._repoid$$", "$projectsdir/_repoid", $datarepoid);
}
$datarepoid = readstr("$projectsdir/_repoid");

BSStdServer::setup_authenticator();
BSStdServer::server('src_server', \@ARGV, $conf, $aconf);
